diff --git "a/docs/\344\270\223\347\224\250\344\272\216\346\234\254\351\241\271\347\233\256\347\232\204AI prompt.txt" "b/docs/\344\270\223\347\224\250\344\272\216\346\234\254\351\241\271\347\233\256\347\232\204AI prompt.txt" new file mode 100644 index 00000000..3b33fb15 --- /dev/null +++ "b/docs/\344\270\223\347\224\250\344\272\216\346\234\254\351\241\271\347\233\256\347\232\204AI prompt.txt" @@ -0,0 +1,324 @@ +## AI 智能体操作指南 +### 操作原则 +#### 1. 保守修改原则 +- **优先扩展**而非修改现有功能 +- **保持接口稳定**,内部实现可优化 +- **向后兼容**是最高优先级 + +#### 2. 调用链保护原则 +- 任何修改前必须分析完整的调用链 +- 禁止破坏关键路径的方法签名 +- 确保跨模块调用的完整性 + +#### 3. 测试驱动原则 +- 修改前确认测试覆盖 +- 修改后立即运行测试验证 +- 新增功能必须包含相应测试 + +### 具体操作步骤 + +#### 步骤1:修改前的分析(必须执行) +```bash +# 1. 搜索目标方法的所有调用位置 +grep -r "目标方法名" /workspace/N_m3u8DL-RE-src/src/ --include="*.cs" + +# 2. 分析项目依赖关系 +find /workspace/N_m3u8DL-RE-src/src -name "*.csproj" -exec cat {} \; + +# 3. 检查接口实现一致性 +grep -r "interface" /workspace/N_m3u8DL-RE-src/src/ --include="*.cs" | grep -i "目标接口" +``` + +#### 步骤2:安全修改策略 +```csharp +// 策略1:使用可选参数保持兼容 +public async Task ExistingMethod(string requiredParam, string newParam = null) + +// 策略2:使用方法重载 +public async Task ExistingMethod() => ExistingMethod("default"); +public async Task ExistingMethod(string param) { /* 实现 */ } + +// 策略3:创建新方法而非修改旧方法 +public async Task NewMethodWithImprovement() +{ + // 新功能实现 +} +``` + +#### 步骤3:修改后的验证(必须执行) +```bash +# 1. 编译验证 +dotnet build /workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE.sln + +# 2. 测试验证 +dotnet test /workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE.Tests + +# 3. 功能验证(模拟用户操作) +dotnet run --project /workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE --help +``` + +### 常见风险场景及应对 + +#### 风险场景1:方法重命名 +**风险**:调用链断裂,"找不到方法"错误 +**应对**: +```csharp +// 错误做法:直接重命名 +public async Task NewMethodName() // ❌ 破坏调用链 + +// 正确做法:保持旧方法,标记为过时 +[Obsolete("请使用NewMethodName方法")] +public async Task OldMethodName() => NewMethodName(); +public async Task NewMethodName() { /* 新实现 */ } +``` + +#### 风险场景2:参数变更 +**风险**:调用方参数不匹配 +**应对**: +```csharp +// 错误做法:强制要求新参数 +public async Task Method(string newRequiredParam) // ❌ + +// 正确做法:保持可选参数 +public async Task Method(string param = null) // ✅ +{ + param ??= GetDefaultParam(); + // 实现逻辑 +} +``` + +#### 风险场景3:返回类型变更 +**风险**:调用方类型转换错误 +**应对**: +```csharp +// 错误做法:改变返回类型 +public async Task Method() // ❌ + +// 正确做法:添加新方法 +public async Task NewMethod() { /* 新实现 */ } +// 保持旧方法兼容性 +public async Task Method() => await NewMethod().IsSuccess; +``` +#### 风险场景4:报错找不到某文件 +//错误做法:手动新建 +//正确做法:文件可能是被重命名,所以要查询相似文件名的文件是否就是要找的某文件. 若还找不到,则从Git历史查询此文件最后出现在哪一次commit并汇报给用户 +## 项目特定约束和限制 + +### 技术约束 +1. **.NET 9.0限制**:不能使用更高版本的.NET特性 +2. **C# 13.0限制**:语法特性受版本限制 +3. **依赖版本锁定**:System.CommandLine等依赖版本固定 + +### 架构约束 +1. **模块依赖方向**:只能从高层模块依赖低层模块 +2. **接口稳定性**:公共接口必须保持向后兼容 +3. **数据流方向**:数据必须按照既定流程传递 + +### 性能约束 +1. **内存使用**:流媒体处理需要控制内存占用 +2. **网络IO**:下载操作需要合理的并发控制 +3. **文件IO**:大文件处理需要优化IO操作 + +## 最佳实践总结 + +### 代码修改最佳实践 +1. **分析先行**:修改前全面分析影响范围 +2. **测试驱动**:先写测试,再实现功能 +3. **小步提交**:每次修改保持小范围,便于回滚 +4. **文档更新**:修改后及时更新相关文档 + +### 调用链保护最佳实践 +1. **接口契约**:严格遵守接口定义 +2. **方法签名**:保持关键方法签名稳定 +3. **异常处理**:完善的错误处理和恢复机制 +4. **日志记录**:详细的调用链日志记录 + +### 质量保证最佳实践 +1. **编译检查**:每次修改后必须通过编译 +2. **测试覆盖**:确保测试覆盖关键路径 +3. **集成验证**:验证跨模块功能正常 +4. **性能监控**:监控修改对性能的影响 + +## 紧急情况处理 + +### 调用链断裂应急方案 +1. **立即回滚**:恢复到修改前的稳定状态 +2. **日志分析**:分析错误日志定位问题 +3. **测试修复**:编写针对性测试修复问题 +4. **逐步发布**:修复后小范围验证再全面发布 + +### 编译错误处理 +1. **依赖检查**:确认所有依赖项正确引用 +2. **版本兼容**:检查.NET和目标框架版本 +3. **语法验证**:使用IDE工具验证语法正确性 +4. **构建清理**:清理构建缓存重新编译 + +--- + +## 最终提醒 + +**重要提示**:本提示词基于对 N_m3u8DL-RE 项目的深入分析,所有指导原则都旨在保护项目的稳定性和可维护性。在进行任何代码修改时,请务必遵循本提示词中的规范和要求,确保函数调用链的完整性,避免因修改导致的方法引用失效问题。 + +**记住**: +一个稳定的调用链比一个新功能更重要! +默认输入文件/workspace/input.txt +默认输出目录mpegts.js/demo/output +记住与Git相关命令需要用户确认才能执行 +生成的临时文件or测试文件名要带前缀deletable + +# N_m3u8DL-RE 插件系统开发文档 + +本文档介绍如何为 N_m3u8DL-RE 开发和使用插件系统。 + +## 1. 插件系统架构设计 + +插件系统具有以下特点: + +- **非侵入性**: 所有插件代码都放在 `extend` 目录中,不会影响主程序的核心代码 +- **可扩展性**: 通过 [IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89) 接口可以轻松添加新的插件功能 +- **配置驱动**: 插件行为可以通过 [PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json) 配置文件进行管理 +- **事件驱动**: 通过 `OnFileDownloaded` 事件触发插件逻辑 + +## 2. 核心组件 + +### PluginManager.cs (插件管理器) +- 实现了插件的加载、初始化和事件分发功能 +- 支持从配置文件中读取插件设置 +- 提供了统计下载次数的功能 + +### UASwitcherPlugin.cs (UA切换插件) +- 实现了每下载一定数量文件切换一次User-Agent的功能 +- 支持从配置文件中读取自定义User-Agent列表 + +### ProxySwitcherPlugin.cs (代理切换插件) +- 实现了每下载一定数量文件切换一次代理的功能 +- 通过Clash API控制代理切换 + +### BatchDownloadPlugin-and-input-output/BatchDownloadPlugin.cs (批量下载插件) +- 实现了批量下载多个URL的功能 +- 支持从配置文件读取URL列表 +- 自动生成包含原始URL信息的唯一文件名 +- 支持批量进度跟踪和错误处理 +- **配置架构优化**: 直接读取PluginConfig.json,无需中间配置类 + +### PluginConfig.json (配置文件) +- 控制各个插件的启用状态和行为参数 + +## 3. 插件接口规范 + +所有插件都需要实现 [IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89) 接口: + +```csharp +public interface IPlugin +{ + void Initialize(PluginConfig? config); + void OnFileDownloaded(string filePath, int downloadCount); +} +``` + +- `Initialize`: 插件初始化方法,在程序启动时调用 +- `OnFileDownloaded`: 文件下载完成回调,在每个文件下载完成后调用 + +## 4. 集成到主程序 + +插件系统已集成到主程序中: + +### SimpleDownloader.cs 集成 +- 在文件下载完成后添加了插件钩子调用 +- 确保无论下载成功还是跳过已存在的文件都会触发插件事件 + +### N_m3u8DL-RE.csproj 集成 +- 添加了对extend目录中插件文件的引用 +- 确保插件配置文件会被复制到输出目录 + +### Program.cs 集成 +- 在程序入口点初始化插件管理器 + +## 5. 设计优势 + +- **避免冲突**: 所有修改都在extend目录和必要的集成点,与原作者的开发路径完全分离 +- **易于维护**: 插件系统采用模块化设计,便于单独维护和升级 +- **高度可配置**: 通过配置文件可以灵活控制插件的行为 +- **易于扩展**: 可以通过实现[IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89)接口轻松添加新功能 + +## 6. 使用说明 + +要使用这个插件系统: + +1. 确保extend目录中的插件文件被正确编译 +2. 根据需要修改[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json)中的配置 +3. 程序运行时会自动加载启用的插件 +4. 每当一个文件下载完成时,会自动触发相应的插件逻辑 + +### 批量下载插件使用 + +要使用批量下载插件: + +1. 在[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json)中启用BatchDownload +2. 准备URL列表文件(默认路径:`extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt`) +3. 使用命令行参数 `--batch` 启用批量模式 +4. 指定输出目录:`--save-dir /path/to/output` + +**配置架构优化说明**: +- **统一配置源**: 所有配置直接从PluginConfig.json读取,无需中间配置类 +- **消除冲突**: 解决了配置冲突问题,确保单一配置来源 +- **灵活配置**: 支持直接在PluginConfig.json中修改所有参数 + +**命令示例**: +```bash +dotnet run -- --batch --save-dir /workspace/mpegts.js/demo/output +``` + +**输入文件格式**: +``` +# 注释行以#开头 +https://example1.com/video1.m3u8 +https://example2.com/video2.m3u8 +``` + +**输出文件命名**: +批量下载会自动生成包含原始URL信息的唯一文件名,格式为: +`{URL基础名}_batch{索引}_of_{总数}_{时间戳}.{扩展名}` + +## 7. 开发新插件 + +要开发一个新的插件,需要: + +1. 创建新的插件类,实现[IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89)接口 +2. 在[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json)中添加插件配置项 +3. 在[PluginManager.cs](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs)的[LoadPlugins](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L16-L44)方法中添加插件加载逻辑 +4. 编译并测试插件功能 + +## 8. 配置文件说明 + +[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json) 是插件系统的配置文件,支持以下配置项: + +```json +{ + "UASwitcher": { + "Enabled": true, + "UserAgents": [ + "UA1", + "UA2", + "UA3" + ] + }, + "ProxySwitcher": { + "Enabled": true, + "ClashApiUrl": "http://127.0.0.1:9090", + "SwitchInterval": 3 + }, + "BatchDownload": { + "Enabled": true, + "CreateSubdirectories": false, + "MaxConcurrency": 3 + } +} +``` + +- `Enabled`: 控制插件是否启用 +- `UserAgents`: UA切换插件使用的User-Agent列表 +- `ClashApiUrl`: 代理切换插件使用的Clash API地址 +- `SwitchInterval`: 切换间隔(每下载多少个文件切换一次) +- `CreateSubdirectories`: 批量下载插件是否为每个URL创建子目录 +- `MaxConcurrency`: 批量下载插件的最大并发数(预留功能) \ No newline at end of file diff --git "a/docs/\345\207\275\346\225\260\350\260\203\347\224\250\345\205\263\347\263\273\345\210\206\346\236\220.md" "b/docs/\345\207\275\346\225\260\350\260\203\347\224\250\345\205\263\347\263\273\345\210\206\346\236\220.md" new file mode 100644 index 00000000..683f5349 --- /dev/null +++ "b/docs/\345\207\275\346\225\260\350\260\203\347\224\250\345\205\263\347\263\273\345\210\206\346\236\220.md" @@ -0,0 +1,336 @@ +# N_m3u8DL-RE 函数调用关系分析 + +## 主程序执行流程分析 + +### 1. 程序入口函数调用链 + +#### Main 函数 (`Program.cs:28`) +```csharp +static async Task Main(string[] args) +{ + // 插件系统初始化 + PluginManager.LoadPlugins() [通过反射调用] + + // 全球化设置 + ResString.CurrentLoc = loc + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc) + + // 命令行处理 + CommandInvoker.InvokeArgs(args, DoWorkAsync) +} +``` + +#### 命令行调用器 (`CommandInvoker.cs`) +```csharp +public static async Task InvokeArgs(string[] args, Func doWorkAsync) +{ + // 解析命令行参数 + MyOption.Parse(args) + + // 执行主工作函数 + await doWorkAsync(option) +} +``` + +### 2. 核心工作函数调用链 + +#### DoWorkAsync 函数 (`Program.cs:114`) +```csharp +static async Task DoWorkAsync(MyOption option) +{ + // 配置验证阶段 + ValidateExternalTools(option) + ValidateProxySettings(option) + ValidateParameterConstraints(option) + + // 流媒体解析阶段 + var extractor = new StreamExtractor(parserConfig) + await extractor.LoadSourceFromUrlAsync(url) + var streams = await extractor.ExtractStreamsAsync() + + // 流选择阶段 + var selectedStreams = SelectStreams(option, streams) + + // 下载执行阶段 + await ExecuteDownload(option, extractor, selectedStreams) +} +``` + +## 关键模块函数调用关系 + +### 流提取器模块 (StreamExtractor) + +#### LoadSourceFromUrlAsync 函数 (`StreamExtractor.cs:32`) +```csharp +public async Task LoadSourceFromUrlAsync(string url) +{ + // URL类型判断 + if (url.StartsWith("file:")) + File.ReadAllTextAsync(uri.LocalPath) + else if (url.StartsWith("http")) + HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers) + else if (File.Exists(url)) + File.ReadAllTextAsync(url) + + // 内容类型检测和提取器选择 + DetectContentType(rawText) + CreateExtractor() +} +``` + +#### ExtractStreamsAsync 函数 +```csharp +public async Task> ExtractStreamsAsync() +{ + // 调用具体提取器 + return await extractor.ExtractStreamsAsync() + + // 内部调用链: + // HLSExtractor.ExtractStreamsAsync() + // → ParseMasterPlaylist() + // → ParseMediaPlaylist() + // → BuildStreamSpec() + + // DASHExtractor.ExtractStreamsAsync() + // → ParseMPD() + // → ExtractAdaptationSets() + // → BuildStreamSpec() +} +``` + +### 下载管理器模块 (DownloadManager) + +#### SimpleDownloadManager.StartDownloadAsync() +```csharp +public async Task StartDownloadAsync() +{ + // 初始化阶段 + PrepareDownloadDirectory() + CreateDownloadTasks() + + // 下载执行阶段 + await ExecuteParallelDownloads() + + // 后处理阶段 + await MergeDownloadedFiles() + await ApplyDecryption() + await PerformMuxing() +} +``` + +#### HTTPLiveRecordManager.StartRecordAsync() +```csharp +public async Task StartRecordAsync() +{ + // 直播流检测 + DetectLiveStream() + + // 录制循环 + while (IsStreamActive) + { + await FetchLiveSegments() + await ProcessLiveSegments() + await WaitForNextSegment() + } + + // 录制后处理 + await FinalizeRecording() +} +``` + +## 工具类函数调用关系 + +### HTTPUtil 工具类 + +#### GetWebSourceAndNewUrlAsync 函数 +```csharp +public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary headers) +{ + // HTTP请求执行 + using var client = CreateHttpClient(headers) + using var response = await client.GetAsync(url) + + // 响应处理 + response.EnsureSuccessStatusCode() + var content = await response.Content.ReadAsStringAsync() + + // URL重定向处理 + var finalUrl = GetFinalUrl(response) + + return (content, finalUrl) +} +``` + +### FilterUtil 工具类 + +#### SelectStreams 函数 +```csharp +public static List SelectStreams(List streams) +{ + // 交互式选择界面 + DisplayStreamSelectionUI(streams) + + // 用户输入处理 + var selectedIndices = GetUserSelections() + + // 流过滤和排序 + return FilterAndSortStreams(streams, selectedIndices) +} +``` + +#### DoFilterKeep 函数 +```csharp +public static List DoFilterKeep(List streams, string? filter) +{ + // 过滤器解析 + var filterConditions = ParseFilterExpression(filter) + + // 流匹配 + return streams.Where(stream => + MatchFilterConditions(stream, filterConditions) + ).ToList() +} +``` + +## 加密解密模块函数调用 + +### AESUtil 工具类 + +#### DecryptAES128 函数 +```csharp +public static byte[] DecryptAES128(byte[] encryptedData, byte[] key, byte[] iv) +{ + // AES解密初始化 + using var aes = Aes.Create() + aes.Key = key + aes.IV = iv + aes.Mode = CipherMode.CBC + aes.Padding = PaddingMode.PKCS7 + + // 解密执行 + using var decryptor = aes.CreateDecryptor() + return decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length) +} +``` + +### MP4DecryptUtil 工具类 + +#### DecryptWithMP4Decrypt 函数 +```csharp +public static async Task DecryptWithMP4Decrypt(string inputFile, string outputFile, string key) +{ + // 外部工具调用 + var processStartInfo = new ProcessStartInfo + { + FileName = "mp4decrypt", + Arguments = $"--key {key} {inputFile} {outputFile}", + UseShellExecute = false + } + + // 进程执行和监控 + using var process = Process.Start(processStartInfo) + await process.WaitForExitAsync() + + return process.ExitCode == 0 +} +``` + +## 插件系统函数调用 + +### PluginManager 类 + +#### LoadPlugins 函数 +```csharp +public static void LoadPlugins() +{ + // 插件目录扫描 + var pluginDirectory = GetPluginDirectory() + var pluginFiles = Directory.GetFiles(pluginDirectory, "*.dll") + + // 插件加载和初始化 + foreach (var file in pluginFiles) + { + var assembly = Assembly.LoadFrom(file) + var pluginTypes = assembly.GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t)) + + foreach (var type in pluginTypes) + { + var plugin = Activator.CreateInstance(type) as IPlugin + plugin?.Initialize() + } + } +} +``` + +## 关键函数调用时序图 + +### 点播下载时序 +``` +Main() + → CommandInvoker.InvokeArgs() + → DoWorkAsync() + → StreamExtractor.LoadSourceFromUrlAsync() + → HTTPUtil.GetWebSourceAndNewUrlAsync() + → StreamExtractor.ExtractStreamsAsync() + → HLSExtractor.ParseMasterPlaylist() + → FilterUtil.SelectStreams() + → SimpleDownloadManager.StartDownloadAsync() + → DownloadUtil.DownloadSegments() + → MergeUtil.MergeFiles() + → MP4DecryptUtil.DecryptFiles() +``` + +### 直播录制时序 +``` +Main() + → CommandInvoker.InvokeArgs() + → DoWorkAsync() + → StreamExtractor.LoadSourceFromUrlAsync() + → StreamExtractor.ExtractStreamsAsync() + → HTTPLiveRecordManager.StartRecordAsync() + → DetectLiveStream() [循环] + → FetchLiveSegments() + → ProcessLiveSegments() + → WaitForNextSegment() + → FinalizeRecording() +``` + +## 函数调用深度分析 + +### 最深调用链示例 +``` +Program.Main() + ↓ (1层) +CommandInvoker.InvokeArgs() + ↓ (2层) +Program.DoWorkAsync() + ↓ (3层) +StreamExtractor.LoadSourceFromUrlAsync() + ↓ (4层) +HTTPUtil.GetWebSourceAndNewUrlAsync() + ↓ (5层) +HttpClient.GetAsync() [系统调用] + ↓ (6层) +Socket通信层 [系统深度] +``` + +### 关键异步调用点 +1. **HTTP请求** - `HTTPUtil.GetWebSourceAndNewUrlAsync()` +2. **文件IO** - `File.ReadAllTextAsync()` +3. **下载执行** - `SimpleDownloader.DownloadAsync()` +4. **外部工具调用** - `Process.WaitForExitAsync()` + +## 性能关键函数 + +### 高频调用函数 +1. **Logger.Info()** - 日志记录,频繁调用 +2. **HTTPUtil 相关函数** - HTTP请求,网络IO密集 +3. **Stream处理函数** - 数据流处理,内存操作密集 + +### 耗时操作函数 +1. **网络请求** - HTTPUtil 相关函数 +2. **文件操作** - 大文件读写和合并 +3. **外部工具调用** - ffmpeg/mp4decrypt 进程启动 + +这个函数调用关系分析展示了项目的完整执行流程,从程序启动到下载完成的全过程,帮助理解各个模块之间的协作关系。 \ No newline at end of file diff --git "a/docs/\346\211\271\351\207\217\344\270\213\350\275\275\346\217\222\344\273\266\345\274\200\345\217\221\346\255\245\351\252\244.md" "b/docs/\346\211\271\351\207\217\344\270\213\350\275\275\346\217\222\344\273\266\345\274\200\345\217\221\346\255\245\351\252\244.md" new file mode 100644 index 00000000..95c9afcc --- /dev/null +++ "b/docs/\346\211\271\351\207\217\344\270\213\350\275\275\346\217\222\344\273\266\345\274\200\345\217\221\346\255\245\351\252\244.md" @@ -0,0 +1,329 @@ +# 批量下载插件开发步骤文档 + +## 项目概述 + +本插件旨在为N_m3u8DL-RE添加批量下载功能,支持从文本文件读取多个M3U8 URL并进行批量下载。 + +## 功能需求 + +### 核心功能 +1. **批量URL读取**: 从文本文件读取M3U8 URL列表 +2. **顺序/并行下载**: 支持按顺序或并行下载多个流 +3. **进度管理**: 显示批量下载的整体进度 +4. **错误处理**: 单个URL下载失败不影响其他下载 + +### 配置参数 +- 批量文件路径 +- 并发下载数量 +- 输出目录结构 +- 失败重试机制 + +## 技术架构 + +### 插件架构设计 +``` +BatchDownloadPlugin +├── 实现IPlugin接口 +├── 配置文件集成 +├── URL列表解析器 +├── 批量下载管理器 +└── 进度监控器 +``` + +### 文件结构 +``` +extend/ +├── BatchDownloadPlugin.cs # 批量下载插件主类 +├── BatchDownloadConfig.cs # 批量下载配置类 +├── PluginConfig.json # 更新配置文件 +└── PluginManager.cs # 更新插件管理器 +``` + +## 详细实现步骤 + +### 步骤1: 创建批量下载配置类 +**文件**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/BatchDownloadConfig.cs` + +```csharp +public class BatchDownloadConfig +{ + public bool Enabled { get; set; } = false; + public string BatchFile { get; set; } = "urls.txt"; + public int MaxConcurrentDownloads { get; set; } = 1; + public string OutputDirectory { get; set; } = "batch_output"; + public int RetryCount { get; set; } = 3; + public bool CreateSubdirectories { get; set; } = true; +} +``` + +### 步骤2: 更新插件配置类 +**文件**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/PluginConfig.json` + +在现有配置基础上添加: +```json +{ + "BatchDownload": { + "Enabled": true, + "BatchFile": "urls.txt", + "MaxConcurrentDownloads": 1, + "OutputDirectory": "batch_output", + "RetryCount": 3, + "CreateSubdirectories": true + } +} +``` + +### 步骤3: 创建批量下载插件主类 +**文件**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/BatchDownloadPlugin.cs` + +```csharp +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using N_m3u8DL_RE.Common.Log; + +namespace N_m3u8DL_RE.Plugin +{ + public class BatchDownloadPlugin : IPlugin + { + private BatchDownloadConfig? _config; + private List _urlList = new List(); + private int _currentIndex = 0; + + public void Initialize(PluginConfig? config) + { + _config = config?.BatchDownload; + + if (_config?.Enabled == true) + { + LoadUrlList(); + Logger.Info($"[BatchDownloadPlugin] Loaded {_urlList.Count} URLs from {_config.BatchFile}"); + } + } + + public void OnFileDownloaded(string filePath, int downloadCount) + { + // 批量下载插件的主要逻辑在程序启动时处理 + // 此方法用于处理单个下载完成后的回调(可选) + } + + private void LoadUrlList() + { + if (File.Exists(_config?.BatchFile)) + { + var lines = File.ReadAllLines(_config.BatchFile); + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + if (!string.IsNullOrEmpty(trimmedLine) && !trimmedLine.StartsWith("#")) + { + _urlList.Add(trimmedLine); + } + } + } + } + + public List GetUrlList() => _urlList; + public bool HasUrls() => _urlList.Count > 0; + } +} +``` + +### 步骤4: 更新插件管理器 +**文件**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/PluginManager.cs` + +在`PluginConfig`类中添加: +```csharp +public class PluginConfig +{ + public UASwitcherConfig? UASwitcher { get; set; } + public ProxySwitcherConfig? ProxySwitcher { get; set; } + public BatchDownloadConfig? BatchDownload { get; set; } // 新增 +} +``` + +在`IsPluginEnabled`方法中添加: +```csharp +private static bool IsPluginEnabled(string pluginName) +{ + return pluginName switch + { + "UASwitcher" => _config?.UASwitcher?.Enabled ?? false, + "ProxySwitcher" => _config?.ProxySwitcher?.Enabled ?? false, + "BatchDownload" => _config?.BatchDownload?.Enabled ?? false, // 新增 + _ => false + }; +} +``` + +### 步骤5: 修改主程序输入处理逻辑 +**文件**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/Program.cs` + +在`Main`方法中添加批量下载处理逻辑: + +```csharp +// 在插件系统初始化后,检查是否启用批量下载 +bool batchDownloadEnabled = false; +BatchDownloadPlugin? batchPlugin = null; + +try +{ + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var getConfigMethod = pluginManagerType.GetMethod("GetConfig"); + if (getConfigMethod != null) + { + var config = getConfigMethod.Invoke(null, null) as PluginConfig; + batchDownloadEnabled = config?.BatchDownload?.Enabled ?? false; + + if (batchDownloadEnabled) + { + // 获取批量下载插件实例 + var getPluginsMethod = pluginManagerType.GetMethod("GetPlugins", + BindingFlags.NonPublic | BindingFlags.Static); + if (getPluginsMethod != null) + { + var plugins = getPluginsMethod.Invoke(null, null) as List; + batchPlugin = plugins?.OfType().FirstOrDefault(); + } + } + } + } +} +catch (Exception ex) +{ + Console.WriteLine($"[BatchDownload] Failed to check batch download status: {ex.Message}"); +} + +// 如果启用批量下载且有URL列表,则执行批量下载 +if (batchDownloadEnabled && batchPlugin?.HasUrls() == true) +{ + await ExecuteBatchDownload(batchPlugin, option); + return; +} + +// 否则执行正常的单URL下载流程 +// ... 原有的单URL下载逻辑 +``` + +### 步骤6: 实现批量下载执行方法 +**文件**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/Program.cs` + +添加批量下载执行方法: + +```csharp +private static async Task ExecuteBatchDownload(BatchDownloadPlugin batchPlugin, MyOptions option) +{ + var urls = batchPlugin.GetUrlList(); + Logger.InfoMarkUp($"[BatchDownload] Starting batch download with {urls.Count} URLs"); + + int successCount = 0; + int failCount = 0; + + for (int i = 0; i < urls.Count; i++) + { + var url = urls[i]; + Logger.InfoMarkUp($"[BatchDownload] Processing URL {i + 1}/{urls.Count}: {url}"); + + try + { + // 创建子目录(如果配置允许) + var originalSaveDir = option.SaveDir; + if (batchPlugin.GetConfig()?.CreateSubdirectories == true) + { + var subDir = Path.Combine(originalSaveDir ?? ".", $"batch_item_{i + 1}"); + Directory.CreateDirectory(subDir); + option.SaveDir = subDir; + } + + // 执行单个URL下载(重用现有的下载逻辑) + await ExecuteSingleDownload(url, option); + successCount++; + + // 恢复原始保存目录 + option.SaveDir = originalSaveDir; + } + catch (Exception ex) + { + Logger.ErrorMarkUp($"[BatchDownload] Failed to download URL {i + 1}: {ex.Message}"); + failCount++; + } + } + + Logger.InfoMarkUp($"[BatchDownload] Batch download completed. Success: {successCount}, Failed: {failCount}"); +} + +private static async Task ExecuteSingleDownload(string url, MyOptions option) +{ + // 重用Program.cs中现有的单URL下载逻辑 + // 需要将原有的下载逻辑提取为独立方法 + var parserConfig = CreateParserConfig(option); + var extractor = new StreamExtractor(parserConfig); + + await RetryUtil.WebRequestRetryAsync(async () => + { + await extractor.LoadSourceFromUrlAsync(url); + return true; + }); + + // ... 继续执行后续的流提取和下载逻辑 +} +``` + +### 步骤7: 明确URL输入文件and输出路径 +**输入文件**: `/workspace/input.txt` +**输出路径** /workspace/mpegts.js/demo/output + + +``` + +## 测试计划 + +### 单元测试 +创建的测试文件名前缀为deletable +1. **URL列表解析测试**: 验证各种格式的URL文件解析 +2. **配置加载测试**: 验证配置文件正确加载 +3. **插件初始化测试**: 验证插件正确初始化 + +### 集成测试 +1. **单URL批量测试**: 使用单个URL验证批量下载流程 +2. **多URL顺序测试**: 验证多个URL按顺序下载 +3. **错误处理测试**: 验证单个URL失败不影响其他下载 + +### 功能测试 +1. **输出目录结构**: 验证子目录创建功能 +2. **进度显示**: 验证批量下载进度显示 +3. **日志输出**: 验证详细的日志记录 + +## 部署和集成 + +### 编译要求 +- 确保所有新增文件在N_m3u8DL-RE.csproj中被正确引用 +- 验证插件配置文件被复制到输出目录 + +### 使用说明 +2. 修改PluginConfig.json启用批量下载插件 +3. 运行程序,自动检测并执行批量下载 + +## 风险控制 + +### 兼容性风险 +- 保持与现有插件系统的兼容性 +- 不影响单URL下载功能 + +### 性能风险 +- 控制并发下载数量,避免资源耗尽 +- 实现合理的错误重试机制 + +### 稳定性风险 +- 完善的异常处理机制 +- 单个URL失败不影响整体流程 + +## 后续优化方向(以后再考虑) + +1. **并行下载**: 实现真正的并行下载支持 +2. **断点续传**: 支持批量下载的断点续传 +3. **智能调度**: 根据网络状况动态调整并发数量 +4. **Web界面**: 提供Web界面管理批量下载任务 \ No newline at end of file diff --git "a/docs/\346\236\266\346\236\204\345\210\206\346\236\220.md" "b/docs/\346\236\266\346\236\204\345\210\206\346\236\220.md" new file mode 100644 index 00000000..a1d511d5 --- /dev/null +++ "b/docs/\346\236\266\346\236\204\345\210\206\346\236\220.md" @@ -0,0 +1,317 @@ +# N_m3u8DL-RE 项目架构分析 + +## 项目概述 + +N_m3u8DL-RE 是一个跨平台的 DASH/HLS/MSS 流媒体下载工具,采用 .NET 9.0 开发,具有模块化的架构设计。 + +**项目特点:** +- 支持点播和直播流媒体下载 +- 多格式支持(HLS、DASH、MSS) +- 跨平台兼容(Windows/Linux/macOS) +- 插件化扩展系统 +- 多语言国际化支持 + +## 项目架构 + +### 模块化分层架构 + +项目采用清晰的分层架构,包含4个主要模块: + +``` +N_m3u8DL-RE.sln +├── N_m3u8DL-RE (主程序) - 业务逻辑层 +├── N_m3u8DL-RE.Common (公共模块) - 基础组件层 +├── N_m3u8DL-RE.Parser (解析器) - 数据解析层 +└── N_m3u8DL-RE.Tests (测试) - 测试层 +``` + +### 模块职责划分 + +#### 1. 主程序模块 (N_m3u8DL-RE) +**核心职责:程序入口、命令行处理、下载流程控制** + +**目录结构:** +``` +N_m3u8DL-RE/ +├── Program.cs (19260行) - 主程序入口 +├── CommandLine/ - 命令行参数解析 +│ ├── CommandInvoker.cs - 命令调用器 +│ ├── ComplexParamParser.cs - 复杂参数解析 +│ └── MyOption.cs - 选项配置类 +├── DownloadManager/ - 下载管理器 +│ ├── SimpleDownloadManager.cs - 点播下载管理 +│ ├── HTTPLiveRecordManager.cs - HTTP直播录制 +│ └── SimpleLiveRecordManager2.cs - 普通直播录制 +├── Downloader/ - 下载器接口 +│ ├── IDownloader.cs - 下载器接口 +│ └── SimpleDownloader.cs - 简单下载器实现 +├── Util/ - 工具类集合 +│ ├── FilterUtil.cs - 流过滤器 +│ ├── MergeUtil.cs - 文件合并工具 +│ ├── DownloadUtil.cs - 下载工具 +│ └── 其他工具类... +├── Crypto/ - 加密解密 +│ ├── AESUtil.cs - AES加密工具 +│ ├── ChaCha20Util.cs - ChaCha20加密 +│ └── CSChaCha20.cs - C#实现ChaCha20 +├── Processor/ - URL处理器 +│ ├── DemoProcessor.cs - 示例处理器 +│ ├── DemoProcessor2.cs - 示例处理器2 +│ └── NowehoryzontyUrlProcessor.cs - 特定网站处理器 +├── Entity/ - 数据实体 +│ ├── DownloadResult.cs - 下载结果 +│ ├── StreamFilter.cs - 流过滤器 +│ └── 其他实体类... +├── Enum/ - 枚举定义 +│ ├── DecryptEngine.cs - 解密引擎 +│ ├── MuxFormat.cs - 混流格式 +│ └── SubtitleFormat.cs - 字幕格式 +├── Config/ - 配置管理 +│ ├── EnvConfigKey.cs - 环境配置键 +│ └── DownloaderConfig.cs - 下载器配置 +├── Column/ - 控制台列显示 +│ ├── DownloadStatusColumn.cs - 下载状态列 +│ ├── DownloadSpeedColumn.cs - 下载速度列 +│ └── 其他列显示类... +└── extend/ - 插件系统 + ├── PluginManager.cs - 插件管理器 + ├── UASwitcherPlugin.cs - UA切换插件 + └── ProxySwitcherPlugin.cs - 代理切换插件 +``` + +#### 2. 公共模块 (N_m3u8DL-RE.Common) +**核心职责:提供跨模块共享的基础组件** + +**目录结构:** +``` +N_m3u8DL-RE.Common/ +├── Entity/ - 数据实体定义 +│ ├── StreamSpec.cs - 流规格定义 +│ ├── Playlist.cs - 播放列表 +│ ├── MediaSegment.cs - 媒体片段 +│ ├── EncryptInfo.cs - 加密信息 +│ └── 其他实体类... +├── Enum/ - 枚举类型 +│ ├── ExtractorType.cs - 提取器类型 +│ ├── MediaType.cs - 媒体类型 +│ ├── RoleType.cs - 角色类型 +│ └── EncryptMethod.cs - 加密方法 +├── Util/ - 通用工具类 +│ ├── HTTPUtil.cs - HTTP请求工具 +│ ├── RetryUtil.cs - 重试工具 +│ ├── GlobalUtil.cs - 全局工具 +│ └── HexUtil.cs - 十六进制工具 +├── Log/ - 日志系统 +│ ├── Logger.cs - 日志记录器 +│ ├── LogLevel.cs - 日志级别 +│ └── CustomAnsiConsole.cs - 自定义控制台 +└── Resource/ - 资源管理 + ├── ResString.cs - 资源字符串 + ├── TextContainer.cs - 文本容器 + └── StaticText.cs - 静态文本 +``` + +#### 3. 解析器模块 (N_m3u8DL-RE.Parser) +**核心职责:流媒体格式解析和提取** + +**目录结构:** +``` +N_m3u8DL-RE.Parser/ +├── StreamExtractor.cs - 流提取器主类 +├── Config/ - 解析配置 +│ └── ParserConfig.cs - 解析器配置 +├── Extractor/ - 提取器实现 +│ ├── DASHExtractor.cs - DASH格式提取器 +│ ├── HLSExtractor.cs - HLS格式提取器 +│ └── 其他提取器... +├── Processor/ - 内容处理器 +│ ├── HLS/ - HLS处理器 +│ │ ├── DefaultHLSKeyProcessor.cs - HLS密钥处理器 +│ │ └── DefaultHLSContentProcessor.cs - HLS内容处理器 +│ └── UrlProcessor.cs - URL处理器接口 +└── Util/ - 解析工具 + └── ParserUtil.cs - 解析工具类 +``` + +## 函数调用逻辑分析 + +### 主程序执行流程 + +#### 1. 初始化阶段 (`Program.cs:1-100`) +```csharp +static async Task Main(string[] args) +{ + // 插件系统初始化(反射加载) + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + + // 全球化设置和多语言支持 + string loc = ResString.CurrentLoc; + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc); + + // 命令行参数解析 + await CommandInvoker.InvokeArgs(args, DoWorkAsync); +} +``` + +#### 2. 配置验证阶段 (`Program.cs:100-200`) +```csharp +static async Task DoWorkAsync(MyOption option) +{ + // 检查外部工具 + option.FFmpegBinaryPath ??= GlobalUtil.FindExecutable("ffmpeg"); + + // 验证代理设置 + if (option.CustomProxy != null) + { + HTTPUtil.HttpClientHandler.Proxy = option.CustomProxy; + } + + // 参数互斥性检查 + if (option is { MuxAfterDone: false, MuxImports.Count: > 0 }) + { + throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!"); + } +} +``` + +#### 3. 流媒体解析阶段 (`Program.cs:200-300`) +```csharp +// 创建解析器配置 +var parserConfig = new ParserConfig() +{ + BaseUrl = option.BaseUrl!, + Headers = headers, + CustomMethod = option.CustomHLSMethod, +}; + +// 创建流提取器 +var extractor = new StreamExtractor(parserConfig); + +// 加载源内容 +await extractor.LoadSourceFromUrlAsync(url); + +// 解析流信息 +var streams = await extractor.ExtractStreamsAsync(); +``` + +#### 4. 流选择阶段 (`Program.cs:300-400`) +```csharp +// 流分类和过滤 +var basicStreams = lists.Where(x => x.MediaType is null or MediaType.VIDEO).ToList(); +var audios = lists.Where(x => x.MediaType == MediaType.AUDIO).ToList(); +var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES).ToList(); + +// 自动选择或交互式选择 +if (option.AutoSelect) +{ + // 自动选择逻辑 + selectedStreams.Add(basicStreams.First()); +} +else +{ + // 交互式选择 + selectedStreams = FilterUtil.SelectStreams(lists); +} +``` + +#### 5. 下载执行阶段 (`Program.cs:400-500`) +```csharp +// 根据流类型选择下载管理器 +if (extractor.ExtractorType == ExtractorType.HTTP_LIVE) +{ + var sldm = new HTTPLiveRecordManager(downloadConfig, selectedStreams, extractor); + result = await sldm.StartRecordAsync(); +} +else if (!livingFlag) +{ + var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor); + result = await sdm.StartDownloadAsync(); +} +else +{ + var sldm = new SimpleLiveRecordManager2(downloadConfig, selectedStreams, extractor); + result = await sldm.StartRecordAsync(); +} +``` + +### 核心类调用关系图 + +``` +Program.Main() + ↓ +CommandInvoker.InvokeArgs() + ↓ +Program.DoWorkAsync() + ↓ +StreamExtractor.LoadSourceFromUrlAsync() + ↓ +StreamExtractor.ExtractStreamsAsync() + ↓ +[根据流类型选择下载管理器] + ↓ +HTTPLiveRecordManager.StartRecordAsync() + OR +SimpleDownloadManager.StartDownloadAsync() + OR +SimpleLiveRecordManager2.StartRecordAsync() + ↓ +[下载器执行具体下载任务] + ↓ +[工具类处理文件合并和加密] +``` + +## 技术栈分析 + +### 核心技术依赖 + +**主项目依赖 (N_m3u8DL-RE.csproj):** +- `System.CommandLine` (2.0.0-rc.2.25502.107) - 命令行解析 +- `NiL.JS` (2.6.1706) - JavaScript引擎(插件系统) + +**隐式依赖:** +- `Spectre.Console` - 控制台UI美化 +- `Newtonsoft.Json` - JSON序列化 +- 各种测试框架依赖 + +### 开发环境配置 + +**目标框架:** .NET 9.0 +**语言版本:** C# 13.0 +**可空性:** 启用 +**平台:** AnyCPU, x64 + +## 设计模式应用 + +### 1. 策略模式 (Strategy Pattern) +- **应用场景:** 不同的下载管理器实现 +- **实现:** `HTTPLiveRecordManager`, `SimpleDownloadManager`, `SimpleLiveRecordManager2` + +### 2. 工厂模式 (Factory Pattern) +- **应用场景:** 流提取器创建 +- **实现:** `StreamExtractor` 根据输入自动选择对应的提取器 + +### 3. 观察者模式 (Observer Pattern) +- **应用场景:** 下载进度监控 +- **实现:** 控制台进度条显示和日志系统 + +### 4. 插件模式 (Plugin Pattern) +- **应用场景:** 可扩展的插件系统 +- **实现:** 反射加载插件,支持UA切换、代理切换等功能 + +## 架构特点总结 + +### 优点 +1. **清晰的模块化设计** - 职责分离明确,便于维护 +2. **完善的异步编程** - 全面使用async/await,性能优秀 +3. **强大的错误处理** - 完善的异常处理和重试机制 +4. **国际化支持** - 多语言资源管理,用户体验友好 +5. **跨平台兼容** - 支持主流操作系统 + +### 技术亮点 +1. **插件化架构** - 支持功能扩展 +2. **多格式解析** - 支持HLS、DASH、MSS等多种流媒体格式 +3. **智能流选择** - 自动和交互式流选择机制 +4. **直播录制支持** - 完善的直播流处理能力 +5. **加密解密支持** - 内置多种加密算法支持 + +这个项目展现了良好的软件工程实践,具有清晰的架构设计和完善的功能模块划分,是一个高质量的流媒体下载工具实现。 \ No newline at end of file diff --git "a/docs/\346\250\241\345\235\227\345\212\237\350\203\275\350\257\246\347\273\206\350\257\264\346\230\216.md" "b/docs/\346\250\241\345\235\227\345\212\237\350\203\275\350\257\246\347\273\206\350\257\264\346\230\216.md" new file mode 100644 index 00000000..45ab30dd --- /dev/null +++ "b/docs/\346\250\241\345\235\227\345\212\237\350\203\275\350\257\246\347\273\206\350\257\264\346\230\216.md" @@ -0,0 +1,362 @@ +# N_m3u8DL-RE 模块功能详细说明 + +## 项目模块概览 + +N_m3u8DL-RE 采用模块化设计,将功能划分为4个主要项目模块,每个模块有明确的职责边界。 + +## 1. 主程序模块 (N_m3u8DL-RE) + +### 1.1 程序入口 (Program.cs) +**文件位置:** `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/Program.cs` +**代码行数:** 19260行 + +**核心功能:** +- 程序启动和初始化 +- 命令行参数解析和处理 +- 全局配置和设置 +- 主工作流程控制 +- 错误处理和用户交互 + +**关键函数:** +- `Main()` - 程序入口点 +- `DoWorkAsync()` - 核心工作流程 +- `Console_CancelKeyPress()` - 中断处理 +- `CheckUpdateAsync()` - 版本检查 + +### 1.2 命令行模块 (CommandLine/) + +#### CommandInvoker.cs +**功能:** 命令行参数调用器 +- 解析命令行参数 +- 验证参数有效性 +- 调用相应的处理函数 + +#### ComplexParamParser.cs +**功能:** 复杂参数解析器 +- 处理键值对参数 +- 解析自定义格式参数 +- 参数转换和验证 + +#### MyOption.cs +**功能:** 选项配置类 +- 定义所有命令行选项 +- 参数默认值设置 +- 参数约束验证 + +### 1.3 下载管理器 (DownloadManager/) + +#### SimpleDownloadManager.cs +**功能:** 点播下载管理器 +- 管理点播流下载流程 +- 协调多个下载任务 +- 处理下载进度和状态 + +#### HTTPLiveRecordManager.cs +**功能:** HTTP直播录制管理器 +- 专门处理HTTP直播流 +- 实时监控流状态 +- 分段录制和合并 + +#### SimpleLiveRecordManager2.cs +**功能:** 普通直播录制管理器 +- 处理普通直播流录制 +- 支持实时转码 +- 录制质量控制 + +### 1.4 下载器接口 (Downloader/) + +#### IDownloader.cs +**功能:** 下载器接口定义 +```csharp +public interface IDownloader +{ + Task DownloadAsync(string url, Dictionary headers); + Task CancelAsync(); + event EventHandler ProgressChanged; +} +``` + +#### SimpleDownloader.cs +**功能:** 简单下载器实现 +- 基于HttpClient的下载实现 +- 支持断点续传 +- 进度监控和报告 + +### 1.5 工具类集合 (Util/) + +#### FilterUtil.cs +**功能:** 流过滤器工具 +- 流选择逻辑实现 +- 过滤条件解析 +- 交互式选择界面 + +#### MergeUtil.cs +**功能:** 文件合并工具 +- 媒体文件合并 +- 支持多种合并方式 +- 合并进度监控 + +#### DownloadUtil.cs +**功能:** 下载工具 +- 分段下载管理 +- 并发下载控制 +- 下载错误处理 + +#### 其他工具类: +- `ImageHeaderUtil.cs` - 图像头信息处理 +- `MediainfoUtil.cs` - 媒体信息获取 +- `SubtitleUtil.cs` - 字幕处理 +- `PipeUtil.cs` - 管道通信 +- `LanguageCodeUtil.cs` - 语言代码处理 +- `OtherUtil.cs` - 杂项工具 + +### 1.6 加密解密模块 (Crypto/) + +#### AESUtil.cs +**功能:** AES加密解密工具 +- AES-128/CBC模式加解密 +- 密钥和IV处理 +- 错误处理和安全检查 + +#### ChaCha20Util.cs +**功能:** ChaCha20加密工具 +- ChaCha20流加密实现 +- 兼容各种ChaCha20变体 +- 高性能流处理 + +#### CSChaCha20.cs +**功能:** C#实现的ChaCha20 +- 纯C#实现的ChaCha20算法 +- 避免外部依赖 +- 跨平台兼容 + +### 1.7 URL处理器 (Processor/) + +#### DemoProcessor.cs +**功能:** 示例URL处理器 +- 演示URL处理流程 +- 提供处理模板 +- 测试和示例用途 + +#### NowehoryzontyUrlProcessor.cs +**功能:** 特定网站URL处理器 +- 针对nowehoryzonty.pl网站定制 +- 特殊URL解析逻辑 +- 网站特定需求处理 + +### 1.8 插件系统 (extend/) + +#### PluginManager.cs +**功能:** 插件管理器 +- 插件加载和初始化 +- 插件生命周期管理 +- 插件间通信协调 + +#### UASwitcherPlugin.cs +**功能:** UA切换插件 +- 用户代理字符串管理 +- 动态UA切换 +- 反爬虫策略支持 + +#### ProxySwitcherPlugin.cs +**功能:** 代理切换插件 +- 代理服务器管理 +- 自动代理切换 +- 代理池支持 + +## 2. 公共模块 (N_m3u8DL-RE.Common) + +### 2.1 数据实体 (Entity/) + +#### StreamSpec.cs +**功能:** 流规格定义 +```csharp +public class StreamSpec +{ + public string? Id { get; set; } + public MediaType? MediaType { get; set; } + public string? Codecs { get; set; } + public string? Language { get; set; } + public string? Resolution { get; set; } + public long Bandwidth { get; set; } + public Playlist? Playlist { get; set; } +} +``` + +#### Playlist.cs +**功能:** 播放列表实体 +- 主播放列表和媒体播放列表 +- 分段信息管理 +- 加密信息存储 + +#### MediaSegment.cs +**功能:** 媒体分段实体 +- 分段URL和时长 +- 加密状态和密钥 +- 时间戳和序号 + +### 2.2 枚举类型 (Enum/) + +#### ExtractorType.cs +**功能:** 提取器类型枚举 +```csharp +public enum ExtractorType +{ + UNKNOWN, + HTTP_LIVE, // HLS + MPEG_DASH, // DASH + MSS // Smooth Streaming +} +``` + +#### MediaType.cs +**功能:** 媒体类型枚举 +- VIDEO - 视频流 +- AUDIO - 音频流 +- SUBTITLES - 字幕流 + +### 2.3 通用工具类 (Util/) + +#### HTTPUtil.cs +**功能:** HTTP请求工具 +- 统一的HTTP客户端管理 +- 请求重试机制 +- 响应处理工具 + +#### RetryUtil.cs +**功能:** 重试工具 +- 指数退避重试策略 +- 网络请求重试 +- 错误类型识别 + +#### GlobalUtil.cs +**功能:** 全局工具 +- 系统路径查找 +- 文件操作工具 +- 系统信息获取 + +### 2.4 日志系统 (Log/) + +#### Logger.cs +**功能:** 日志记录器 +- 多级别日志记录 +- 文件和控制台输出 +- 彩色日志显示 + +#### CustomAnsiConsole.cs +**功能:** 自定义控制台 +- ANSI转义序列处理 +- 跨平台控制台美化 +- 进度条和状态显示 + +### 2.5 资源管理 (Resource/) + +#### ResString.cs +**功能:** 资源字符串管理 +- 多语言字符串存储 +- 动态语言切换 +- 字符串格式化 + +## 3. 解析器模块 (N_m3u8DL-RE.Parser) + +### 3.1 流提取器主类 (StreamExtractor.cs) + +**核心功能:** +- 统一流提取接口 +- 自动格式检测 +- 多格式兼容处理 + +**关键方法:** +- `LoadSourceFromUrlAsync()` - 加载源内容 +- `ExtractStreamsAsync()` - 提取流信息 +- `FetchPlayListAsync()` - 获取播放列表 + +### 3.2 提取器实现 (Extractor/) + +#### HLSExtractor.cs +**功能:** HLS格式提取器 +- M3U8文件解析 +- 主播放列表处理 +- 媒体播放列表解析 + +#### DASHExtractor.cs +**功能:** DASH格式提取器 +- MPD文件解析 +- 自适应流处理 +- 分段信息提取 + +### 3.3 内容处理器 (Processor/) + +#### DefaultHLSKeyProcessor.cs +**功能:** HLS密钥处理器 +- 密钥URL解析 +- 密钥获取和解密 +- 密钥缓存管理 + +#### DefaultHLSContentProcessor.cs +**功能:** HLS内容处理器 +- 内容解密处理 +- 分段重组 +- 错误恢复 + +## 4. 测试模块 (N_m3u8DL-RE.Tests) + +### 4.1 单元测试 + +#### DASHExtractor2Tests.cs +**功能:** DASH提取器测试 +- 解析功能测试 +- 边界条件测试 +- 错误处理测试 + +#### HexUtilTests.cs +**功能:** 十六进制工具测试 +- 编码解码测试 +- 性能测试 +- 兼容性测试 + +### 4.2 测试资源管理 + +#### ResourceHelper.cs +**功能:** 测试资源助手 +- 测试数据管理 +- 模拟响应生成 +- 测试环境设置 + +## 模块间依赖关系 + +### 依赖关系图 +``` +N_m3u8DL-RE (主程序) + ├── N_m3u8DL-RE.Parser (解析器) [强依赖] + └── N_m3u8DL-RE.Common (公共模块) [强依赖] + +N_m3u8DL-RE.Parser (解析器) + └── N_m3u8DL-RE.Common (公共模块) [强依赖] + +N_m3u8DL-RE.Tests (测试) + ├── N_m3u8DL-RE (主程序) [测试依赖] + ├── N_m3u8DL-RE.Parser (解析器) [测试依赖] + └── N_m3u8DL-RE.Common (公共模块) [测试依赖] +``` + +### 数据流关系 +1. **配置数据流:** CommandLine → Program → 各模块 +2. **解析数据流:** Parser → Common.Entity → DownloadManager +3. **下载数据流:** DownloadManager → Downloader → 文件系统 +4. **日志数据流:** 所有模块 → Common.Log → 控制台/文件 + +## 模块扩展性设计 + +### 插件扩展点 +1. **URL处理器** - 通过Processor接口扩展 +2. **下载器实现** - 通过IDownloader接口扩展 +3. **内容处理器** - 通过ContentProcessor接口扩展 +4. **加密解密器** - 通过Crypto接口扩展 + +### 配置扩展性 +1. **命令行参数** - 通过MyOption类扩展 +2. **解析配置** - 通过ParserConfig类扩展 +3. **下载配置** - 通过DownloaderConfig类扩展 + +这个模块功能详细说明提供了每个模块的完整功能描述,帮助理解项目的架构设计和各模块的职责划分。 \ No newline at end of file diff --git "a/docs/\351\241\271\347\233\256\346\200\273\347\273\223.md" "b/docs/\351\241\271\347\233\256\346\200\273\347\273\223.md" new file mode 100644 index 00000000..02cb8577 --- /dev/null +++ "b/docs/\351\241\271\347\233\256\346\200\273\347\273\223.md" @@ -0,0 +1,196 @@ +# N_m3u8DL-RE 项目总结 + +## 项目概述 + +N_m3u8DL-RE 是一个功能强大的跨平台流媒体下载工具,专门用于下载 DASH、HLS 和 MSS 格式的流媒体内容。项目采用现代化的 .NET 9.0 技术栈,具有清晰的模块化架构和优秀的工程实践。 + +## 核心特性 + +### 支持的流媒体格式 +- **HLS (HTTP Live Streaming)** - Apple 的流媒体协议 +- **DASH (Dynamic Adaptive Streaming over HTTP)** - MPEG 标准协议 +- **MSS (Microsoft Smooth Streaming)** - 微软流媒体协议 + +### 功能特性 +- ✅ 点播和直播流媒体下载 +- ✅ 自动流选择和质量检测 +- ✅ 多语言字幕支持 +- ✅ 加密内容解密支持 +- ✅ 断点续传和并发下载 +- ✅ 实时下载进度监控 +- ✅ 插件化扩展系统 +- ✅ 跨平台兼容性 + +## 技术架构总结 + +### 架构特点 + +1. **模块化分层设计** + - 清晰的职责分离(主程序、公共模块、解析器、测试) + - 松耦合的模块间通信 + - 易于维护和扩展 + +2. **异步编程模型** + - 全面使用 async/await 模式 + - 高性能的并发处理 + - 良好的用户体验(非阻塞操作) + +3. **插件化架构** + - 反射加载插件机制 + - 可扩展的功能接口 + - 灵活的定制能力 + +### 设计模式应用 + +| 设计模式 | 应用场景 | 实现示例 | +|---------|---------|---------| +| 策略模式 | 下载管理器选择 | HTTPLiveRecordManager vs SimpleDownloadManager | +| 工厂模式 | 流提取器创建 | StreamExtractor 自动选择对应提取器 | +| 观察者模式 | 进度监控 | 下载进度事件监听 | +| 插件模式 | 功能扩展 | 插件加载和初始化 | + +## 代码质量评估 + +### 优点 +1. **代码组织良好** + - 清晰的目录结构 + - 合理的文件命名 + - 一致的代码风格 + +2. **错误处理完善** + - 全面的异常捕获 + - 友好的错误信息 + - 重试机制和容错处理 + +3. **文档和注释** + - 关键函数有详细注释 + - 复杂的逻辑有说明 + - 易于理解和维护 + +4. **性能优化** + - 异步操作避免阻塞 + - 合理的资源管理 + - 高效的算法实现 + +### 改进建议 +1. **测试覆盖度** + - 增加更多单元测试 + - 集成测试覆盖主要流程 + - 性能测试和压力测试 + +2. **配置管理** + - 支持配置文件方式 + - 环境变量配置支持 + - 配置验证和默认值 + +3. **日志系统** + - 结构化日志输出 + - 日志级别动态调整 + - 日志文件轮转管理 + +## 技术栈深度分析 + +### 核心技术组件 + +| 组件 | 版本 | 用途 | 重要性 | +|------|------|------|--------| +| .NET | 9.0 | 运行时框架 | ⭐⭐⭐⭐⭐ | +| System.CommandLine | 2.0.0 | 命令行解析 | ⭐⭐⭐⭐ | +| NiL.JS | 2.6.1706 | JavaScript引擎 | ⭐⭐⭐ | +| Spectre.Console | 最新 | 控制台美化 | ⭐⭐⭐ | + +### 开发工具链 +- **构建工具**: .NET SDK 9.0 +- **测试框架**: xUnit/NUnit +- **包管理**: NuGet +- **版本控制**: Git + +## 项目成熟度评估 + +### 功能完整性: 90% +- 核心下载功能完善 +- 多格式支持良好 +- 扩展机制健全 + +### 代码质量: 85% +- 架构设计合理 +- 代码规范良好 +- 错误处理完善 + +### 文档完整性: 70% +- 代码注释良好 +- 使用文档需要完善 +- API文档需要补充 + +### 测试覆盖度: 60% +- 基础单元测试存在 +- 集成测试需要加强 +- 自动化测试需要完善 + +## 使用场景分析 + +### 适用场景 +1. **个人媒体下载** - 下载在线视频课程、电影等 +2. **内容备份** - 备份付费流媒体内容 +3. **离线观看** - 在没有网络的环境下观看 +4. **内容分析** - 分析流媒体格式和结构 + +### 技术适用性 +- **开发者**: 适合有 .NET 开发经验的用户 +- **技术用户**: 命令行界面,需要一定技术基础 +- **普通用户**: 需要学习命令行参数使用 + +## 项目发展建议 + +### 短期改进(1-3个月) +1. **完善文档系统** + - 编写详细的使用指南 + - 添加API文档 + - 创建故障排除手册 + +2. **增强测试覆盖** + - 增加集成测试 + - 添加性能测试 + - 完善错误场景测试 + +3. **用户体验优化** + - 简化命令行参数 + - 添加交互式模式 + - 改进错误信息提示 + +### 中期规划(3-12个月) +1. **功能扩展** + - 支持更多流媒体协议 + - 添加图形界面版本 + - 增强插件生态系统 + +2. **性能优化** + - 下载速度优化 + - 内存使用优化 + - 并发处理优化 + +3. **生态建设** + - 建立社区支持 + - 创建插件市场 + - 提供云服务集成 + +### 长期愿景(1年以上) +1. **平台化发展** + - 微服务架构重构 + - 分布式下载支持 + - 云原生部署 + +2. **智能化增强** + - AI驱动的流选择 + - 智能质量优化 + - 预测性下载 + +## 总结 + +N_m3u8DL-RE 是一个技术成熟、架构优秀的流媒体下载工具。项目展现了良好的软件工程实践,具有清晰的模块划分和完善的功能实现。虽然在某些方面(如测试覆盖度和文档完整性)还有改进空间,但整体而言是一个高质量的开源项目。 + +项目的模块化设计和插件化架构为未来的功能扩展提供了良好的基础,异步编程模型确保了高性能的用户体验。对于需要下载流媒体内容的用户来说,这是一个值得推荐的工具。 + +**项目评级: ⭐⭐⭐⭐☆ (4.5/5)** + +*注: 评级基于代码质量、功能完整性、架构设计、文档完善度等多个维度综合评估* \ No newline at end of file diff --git a/img/RE.gif b/img/RE.gif deleted file mode 100644 index 60654b0b..00000000 Binary files a/img/RE.gif and /dev/null differ diff --git a/img/RE2.gif b/img/RE2.gif deleted file mode 100644 index 94be04a9..00000000 Binary files a/img/RE2.gif and /dev/null differ diff --git a/src/N_m3u8DL-RE.Common/Log/Logger.cs b/src/N_m3u8DL-RE.Common/Log/Logger.cs index c7d88c76..66f82002 100644 --- a/src/N_m3u8DL-RE.Common/Log/Logger.cs +++ b/src/N_m3u8DL-RE.Common/Log/Logger.cs @@ -102,9 +102,10 @@ private static void HandleLog(string write, string subWrite = "") LogWriteLock.ExitWriteLock(); } } - catch (Exception) + catch (Exception ex) { - Console.WriteLine("Failed to write: " + write); + Console.WriteLine($"Failed to write log: {ex.GetType().Name} - {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); } } diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index aa39dc66..08b8e01f 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -4,11 +4,13 @@ using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Enum; +using N_m3u8DL_RE.Plugin; using N_m3u8DL_RE.Util; using System.CommandLine; using System.CommandLine.Parsing; using System.Globalization; using System.Net; +using System.Reflection; using System.Text.RegularExpressions; namespace N_m3u8DL_RE.CommandLine; @@ -30,7 +32,8 @@ internal static partial class CommandInvoker [GeneratedRegex("^[0-9a-fA-f]{32}$")] private static partial Regex SingleHexKeyRegex(); - private static readonly Argument Input = new("input") { Description = ResString.cmd_Input }; + private static readonly Option Input = new("--input", "-i") { Description = ResString.cmd_Input }; + private static readonly Option BatchMode = new("--batch") { Description = "Enable batch download mode" }; private static readonly Option TmpDir = new("--tmp-dir") { Description = ResString.cmd_tmpDir }; private static readonly Option SaveDir = new("--save-dir") { Description = ResString.cmd_saveDir }; private static readonly Option SaveName = new("--save-name") { Description = ResString.cmd_saveName, CustomParser = ParseSaveName}; @@ -612,7 +615,8 @@ private static MyOption GetOptions(ParseResult result) { var option = new MyOption { - Input = result.GetRequiredValue(Input), + Input = result.GetValue(Input), + BatchMode = result.GetValue(BatchMode), ForceAnsiConsole = result.GetValue(ForceAnsiConsole), NoAnsiColor = result.GetValue(NoAnsiColor), LogLevel = result.GetValue(LogLevel), @@ -706,6 +710,47 @@ private static MyOption GetOptions(ParseResult result) public static async Task InvokeArgs(string[] args, Func action) { + // 【输入流拦截】由PluginManager.cs统一管理和调用 + try + { + // 参数拦截 + args = InputStreamInterceptor.InterceptArgs(args); + + // 选项拦截 - 简化实现,避免复杂的CommandLineBuilder API + try + { + // 直接解析参数用于插件通知 + object? option = args; + + // 插件输入事件通知 + try + { + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var notifyInputMethod = pluginManagerType.GetMethod("NotifyPluginsOnInput", + BindingFlags.NonPublic | BindingFlags.Static); + if (notifyInputMethod != null) + { + notifyInputMethod.Invoke(null, new object[] { args, option }); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Plugin notification failed: {ex.Message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Failed to process options: {ex.Message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Failed to process input: {ex.Message}"); + } + var argList = new List(args); var index = -1; if ((index = argList.IndexOf("--morehelp")) >= 0 && argList.Count > index + 1) @@ -727,7 +772,7 @@ public static async Task InvokeArgs(string[] args, Func act var rootCommand = new RootCommand(VERSION_INFO) { - Input, TmpDir, SaveDir, SaveName, SavePattern, LogFilePath, BaseUrl, ThreadCount, DownloadRetryCount, HttpRequestTimeout, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, + Input, BatchMode, TmpDir, SaveDir, SaveName, SavePattern, LogFilePath, BaseUrl, ThreadCount, DownloadRetryCount, HttpRequestTimeout, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, BinaryMerge, UseFFmpegConcatDemuxer, DelAfterDone, NoDateInfo, NoLog, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, SubOnly, SubtitleFormat, AutoSubtitleFix, FFmpegBinaryPath, LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionEngine, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 198187ba..6d026919 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -1,4 +1,4 @@ -using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Enum; @@ -9,9 +9,13 @@ namespace N_m3u8DL_RE.CommandLine; internal class MyOption { /// - /// See: . + /// See: /// - public string Input { get; set; } = default!; + public string? Input { get; set; } + /// + /// See: + /// + public bool BatchMode { get; set; } /// /// See: . /// diff --git a/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs b/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs index 35deacfb..d7916443 100644 --- a/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs +++ b/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs @@ -89,6 +89,8 @@ public SimpleDownloader(DownloaderConfig config) if (File.Exists(des)) { speedContainer.Add(new FileInfo(des).Length); + // 触发插件事件 - 文件已存在 + TriggerPluginEvent(des); return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des }); } @@ -97,6 +99,8 @@ public SimpleDownloader(DownloaderConfig config) if (File.Exists(dec)) { speedContainer.Add(new FileInfo(dec).Length); + // 触发插件事件 - 文件已解密 + TriggerPluginEvent(dec); return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec }); } @@ -119,6 +123,13 @@ public SimpleDownloader(DownloaderConfig config) // 调用下载 var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition); + + // 触发插件事件 - 文件下载完成 + if (result != null) + { + TriggerPluginEvent(result.ActualFilePath); + } + return (des, result); throw new Exception("please retry"); @@ -151,4 +162,28 @@ public SimpleDownloader(DownloaderConfig config) } } } + + /// + /// 触发插件事件 + /// + /// 文件路径 + private void TriggerPluginEvent(string filePath) + { + try + { + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var onFileDownloadedMethod = pluginManagerType.GetMethod("OnFileDownloaded"); + if (onFileDownloadedMethod != null) + { + onFileDownloadedMethod.Invoke(null, new object[] { filePath }); + } + } + } + catch (Exception ex) + { + Logger.Warn($"[Plugin] Failed to trigger plugin event: {ex.Message}"); + } + } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj b/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj index d4fa4d21..7528f9d0 100644 --- a/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj +++ b/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj @@ -1,4 +1,4 @@ - + Exe @@ -19,9 +19,13 @@ + + + + - + \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index fc3ece1f..3bdbd1f6 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Enum; @@ -8,6 +8,7 @@ using N_m3u8DL_RE.Common.Log; using System.Text; using N_m3u8DL_RE.Common.Util; +using N_m3u8DL_RE.Plugin; using N_m3u8DL_RE.Processor; using N_m3u8DL_RE.Config; using N_m3u8DL_RE.Util; @@ -15,6 +16,9 @@ using N_m3u8DL_RE.CommandLine; using System.Net; using N_m3u8DL_RE.Enum; +using System.Reflection; +using System.Linq; +using System.Collections.Generic; namespace N_m3u8DL_RE; @@ -22,6 +26,115 @@ internal class Program { static async Task Main(string[] args) { + // 初始化插件系统 + // 由于命名空间问题,直接通过反射调用 + try + { + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var loadPluginsMethod = pluginManagerType.GetMethod("LoadPlugins"); + if (loadPluginsMethod != null) + { + loadPluginsMethod.Invoke(null, null); + Console.WriteLine("[Plugin] Plugin system initialized"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Failed to initialize plugin system: {ex.Message}"); + } + + // 【日志拦截】由PluginManager.cs统一调用此方法进行日志流拦截初始化 + // 初始化日志拦截器,重定向Console输出到拦截器 + try + { + // 首先尝试通过Assembly.GetExecutingAssembly()获取当前程序集 + var executingAssembly = Assembly.GetExecutingAssembly(); + var logInterceptorType = executingAssembly.GetType("N_m3u8DL_RE.Plugin.LogStreamInterceptor"); + + if (logInterceptorType == null) + { + // 如果找不到,尝试通过Type.GetType + logInterceptorType = Type.GetType("N_m3u8DL_RE.Plugin.LogStreamInterceptor, N_m3u8DL-RE"); + } + + Console.WriteLine($"[LogInterceptor] 正在查找LogStreamInterceptor类型: {logInterceptorType != null}"); + + if (logInterceptorType != null) + { + // 检查配置是否启用StreamInterceptor + bool isLogInterceptorEnabled = false; + try + { + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var getConfigMethod = pluginManagerType.GetMethod("GetConfig"); + if (getConfigMethod != null) + { + var config = getConfigMethod.Invoke(null, null); + if (config != null) + { + var configType = config.GetType(); + var streamInterceptorProp = configType.GetProperty("StreamInterceptor"); + if (streamInterceptorProp != null) + { + var streamConfig = streamInterceptorProp.GetValue(config); + if (streamConfig != null) + { + var enabledProp = streamConfig.GetType().GetProperty("Enabled"); + if (enabledProp != null) + { + isLogInterceptorEnabled = (bool)(enabledProp.GetValue(streamConfig) ?? false); + } + } + } + } + } + } + } + catch (Exception configEx) + { + Console.WriteLine($"[LogInterceptor] 配置检查失败,使用默认设置: {configEx.Message}"); + } + + Console.WriteLine($"[LogInterceptor] StreamInterceptor配置启用状态: {isLogInterceptorEnabled}"); + + var initializeMethod = logInterceptorType.GetMethod("Initialize"); + if (initializeMethod != null) + { + Console.WriteLine("[LogInterceptor] 找到Initialize方法,正在调用..."); + try + { + // 根据配置决定是否启用日志拦截器 + initializeMethod.Invoke(null, new object[] { isLogInterceptorEnabled }); + Console.WriteLine($"[LogInterceptor] 日志拦截器已{(isLogInterceptorEnabled ? "启用" : "禁用")}"); + } + catch (Exception invokeEx) + { + Console.WriteLine($"[LogInterceptor] Initialize方法调用异常: {invokeEx.Message}"); + Console.WriteLine($"[LogInterceptor] 异常详情: {invokeEx.StackTrace}"); + } + // 初始化信息由LogStreamInterceptor内部输出 + } + else + { + Console.WriteLine("[LogInterceptor] 未找到Initialize方法"); + } + } + else + { + Console.WriteLine("[LogInterceptor] 未找到LogStreamInterceptor类型"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + Console.WriteLine($"[LogInterceptor] 异常详情: {ex.StackTrace}"); + } + // 处理NT6.0及以下System.CommandLine报错CultureNotFound问题 if (OperatingSystem.IsWindows()) { @@ -237,6 +350,93 @@ static async Task DoWorkAsync(MyOption option) } } + // 检查是否启用批量下载插件 + bool batchDownloadEnabled = false; + dynamic? batchPlugin = null; + + try + { + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + // 使用新的配置提取方法获取批处理下载启用状态 + var extractEnabledMethod = pluginManagerType.GetMethod("ExtractBatchDownloadEnabledFromConfig", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + if (extractEnabledMethod != null) + { + batchDownloadEnabled = (bool?)extractEnabledMethod.Invoke(null, null) ?? false; + } + + // 获取批量下载插件实例(无论是否启用,因为用户可能显式使用--batch参数) + try + { + var getPluginsMethod = pluginManagerType.GetMethod("GetPlugins"); + if (getPluginsMethod != null) + { + var plugins = getPluginsMethod.Invoke(null, null) as List; + if (plugins != null) + { + foreach (var plugin in plugins) + { + if (plugin.GetType().Name == "BatchDownloadPlugin") + { + batchPlugin = plugin; + break; + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[BatchDownload] Failed to get plugins: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[BatchDownload] Failed to check batch download status: {ex.Message}"); + } + + // 如果启用批量下载且有URL列表,或者用户显式指定了批量模式,则执行批量下载 + if (option.BatchMode || (batchDownloadEnabled && batchPlugin != null)) + { + Console.WriteLine($"[BatchDownload] Detecting batch mode... BatchMode={option.BatchMode}, PluginEnabled={batchDownloadEnabled}, PluginInstance={batchPlugin != null}"); + + // 如果用户显式指定了批量模式,则执行批量下载 + if (option.BatchMode) + { + Console.WriteLine("[BatchDownload] Batch mode explicitly enabled by user"); + await ExecuteBatchDownload(batchPlugin, option); + return; + } + else if (batchPlugin != null) + { + var hasUrlsMethod = batchPlugin.GetType().GetMethod("HasUrls"); + if (hasUrlsMethod?.Invoke(batchPlugin, null) as bool? == true) + { + Console.WriteLine("[BatchDownload] Batch download plugin detected and has URLs"); + await ExecuteBatchDownload(batchPlugin, option); + return; + } + else + { + Console.WriteLine("[BatchDownload] Plugin detected but no URLs available"); + } + } + else + { + Console.WriteLine("[BatchDownload] Batch download configuration found but plugin instance not available"); + } + } + + // 如果没有输入URL且没有启用批量下载,显示帮助信息 + if (string.IsNullOrEmpty(option.Input) && !option.BatchMode) + { + Console.WriteLine("Error: No input URL provided and batch download is not enabled."); + Console.WriteLine("Please provide a URL with --input or use --batch to enable batch download mode"); + return; + } + var url = option.Input; // 流提取器配置 @@ -267,7 +467,8 @@ await RetryUtil.WebRequestRetryAsync(async () => } // 生成文件夹 - var tmpDir = Path.Combine(option.TmpDir ?? Environment.CurrentDirectory, $"{option.SaveName ?? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}"); + var baseDir = option.SaveDir ?? option.TmpDir ?? Environment.CurrentDirectory; + var tmpDir = Path.Combine(baseDir, $"{option.SaveName ?? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}"); // 记录文件 if (option.WriteMetaJson) { @@ -443,6 +644,328 @@ private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor ex } } + static async Task ExecuteBatchDownload(dynamic batchPlugin, MyOption option) + { + List urls = new List(); + bool createSubdirectories = false; + + try + { + // 如果有batchPlugin,从插件获取URL列表和配置 + if (batchPlugin != null) + { + Logger.Info($"[BatchDebug] batchPlugin type: {batchPlugin.GetType().Name}"); + + try + { + if (batchPlugin is N_m3u8DL_RE.Plugin.BatchDownloadPlugin realPlugin) + { + urls = realPlugin.GetUrlList(); + var config = realPlugin.GetConfig(); + + // 从匿名对象中提取CreateSubdirectories配置 + var createSubdirsProperty = config?.GetType().GetProperty("CreateSubdirectories"); + createSubdirectories = createSubdirsProperty?.GetValue(config) as bool? == true; + + // 获取输出目录配置并设置到option中 + var outputDirectory = realPlugin.GetOutputDirectory(); + if (!string.IsNullOrEmpty(outputDirectory) && Directory.Exists(outputDirectory)) + { + // 如果用户没有显式指定输出目录,使用配置文件中的目录 + if (string.IsNullOrEmpty(option.SaveDir)) + { + option.SaveDir = outputDirectory; + Logger.Info($"[BatchDownload] Using configured output directory: {outputDirectory}"); + } + else + { + Logger.Info($"[BatchDownload] Using user-specified output directory: {option.SaveDir}"); + } + } + + Logger.Info($"[BatchDebug] Direct method calls successful. URLs: {urls.Count}, CreateSubdirectories: {createSubdirectories}"); + } + else + { + // 备用反射调用方法 + var getUrlListMethod = batchPlugin.GetType().GetMethod("GetUrlList"); + var getConfigMethod = batchPlugin.GetType().GetMethod("GetConfig"); + + if (getUrlListMethod != null) + { + urls = getUrlListMethod.Invoke(batchPlugin, null) as List ?? new List(); + } + + if (getConfigMethod != null) + { + var config = getConfigMethod.Invoke(batchPlugin, null); + var createSubdirsProperty = config?.GetType().GetProperty("CreateSubdirectories"); + createSubdirectories = createSubdirsProperty?.GetValue(config) as bool? == true; + } + + Logger.Info($"[BatchDebug] Reflection method calls. URLs: {urls.Count}, CreateSubdirectories: {createSubdirectories}"); + } + } + catch (Exception ex) + { + Logger.Error($"[BatchDebug] Failed to call plugin methods: {ex.Message}"); + } + } + + // 如果没有从插件获取到URL列表,批量下载无法继续 + if (urls.Count == 0) + { + Logger.Error("[BatchDownload] No URLs available. Please check plugin configuration and batch file."); + return; + } + + Logger.Info($"[BatchDownload] Starting batch download with {urls.Count} URLs"); + + int successCount = 0; + int failCount = 0; + + for (int i = 0; i < urls.Count; i++) + { + var url = urls[i]; + Logger.Info($"[BatchDownload] Processing URL {i + 1}/{urls.Count}: {url}"); + + try + { + // 重置SaveName,确保每个URL都能生成唯一文件名 + option.SaveName = null; + + // 创建子目录(如果配置允许) + var originalSaveDir = option.SaveDir; + if (createSubdirectories) + { + var subDir = Path.Combine(originalSaveDir ?? ".", $"batch_item_{i + 1}"); + Directory.CreateDirectory(subDir); + option.SaveDir = subDir; + } + + // 执行单个URL下载,传递批量下载索引和URL信息 + await ExecuteSingleDownload(url, option, i + 1, urls.Count, batchDownload: true); + successCount++; + + // 恢复原始保存目录 + option.SaveDir = originalSaveDir; + } + catch (Exception ex) + { + Logger.Error($"[BatchDownload] Failed to download URL {i + 1}: {ex.Message}"); + failCount++; + } + } + + Logger.Info($"[BatchDownload] Batch download completed. Success: {successCount}, Failed: {failCount}"); + } + catch (Exception ex) + { + Logger.Error($"[BatchDownload] Error during batch download: {ex.Message}"); + } + } + + static async Task ExecuteSingleDownload(string url, MyOption option, int batchIndex = 0, int totalBatches = 0, bool batchDownload = false) + { + bool livingFlag = false; // 声明livingFlag变量 + + // 创建解析器配置 + var headers = new Dictionary() + { + ["user-agent"] = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" + }; + + foreach (var item in option.Headers) + { + headers[item.Key] = item.Value; + } + + var parserConfig = new ParserConfig() + { + AppendUrlParams = option.AppendUrlParams, + UrlProcessorArgs = option.UrlProcessorArgs, + BaseUrl = option.BaseUrl!, + Headers = headers, + CustomMethod = option.CustomHLSMethod, + CustomeKey = option.CustomHLSKey, + CustomeIV = option.CustomHLSIv, + }; + + if (option.AllowHlsMultiExtMap) + { + parserConfig.CustomParserArgs.Add("AllowHlsMultiExtMap", "true"); + } + + // 流提取器配置 + var extractor = new StreamExtractor(parserConfig); + + // 从链接加载内容 + await RetryUtil.WebRequestRetryAsync(async () => + { + await extractor.LoadSourceFromUrlAsync(url); + return true; + }); + + // 解析流信息 + var streams = await extractor.ExtractStreamsAsync(); + + // 设置保存名称 + if (string.IsNullOrEmpty(option.SaveName)) + { + if (batchDownload && batchIndex > 0) + { + // 批量下载模式下生成唯一文件名 + var baseName = GetUniqueFileNameFromUrl(url, batchIndex, totalBatches); + option.SaveName = baseName; + } + else + { + option.SaveName = OtherUtil.GetFileNameFromInput(url); + } + } + + // 生成文件夹 + var baseDir = option.SaveDir ?? option.TmpDir ?? Environment.CurrentDirectory; + var tmpDir = Path.Combine(baseDir, $"{option.SaveName ?? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}"); + + // 记录文件 + if (option.WriteMetaJson) + { + extractor.RawFiles["meta.json"] = GlobalUtil.ConvertToJson(streams); + } + + // 写出文件 + await WriteRawFilesAsync(option, extractor, tmpDir); + + Logger.Info($"Streams info: {streams.Count} streams found"); + + // 选择流(简化逻辑,使用自动选择) + var selectedStreams = new List(); + var basicStreams = streams.Where(x => x.MediaType is null or MediaType.VIDEO).ToList(); + var audios = streams.Where(x => x.MediaType == MediaType.AUDIO).ToList(); + var subs = streams.Where(x => x.MediaType == MediaType.SUBTITLES).ToList(); + + if (basicStreams.Count != 0) + selectedStreams.Add(basicStreams.First()); + + var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language); + foreach (var lang in langs) + { + selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).ThenByDescending(GetOrder).First()); + } + selectedStreams.AddRange(subs); + + if (selectedStreams.Count == 0) + throw new Exception("No streams to download"); + + // 加载播放列表 + if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS) + await extractor.FetchPlayListAsync(selectedStreams); + + // 创建下载管理器并开始下载 + var downloadConfig = new DownloaderConfig() + { + MyOptions = option, + DirPrefix = tmpDir, + Headers = parserConfig.Headers, // 使用命令行解析得到的Headers + }; + + var result = false; + + if (extractor.ExtractorType == ExtractorType.HTTP_LIVE) + { + var sldm = new HTTPLiveRecordManager(downloadConfig, selectedStreams, extractor); + result = await sldm.StartRecordAsync(); + } + else if (!livingFlag) + { + // 开始下载 + var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor); + result = await sdm.StartDownloadAsync(); + } + else + { + var sldm = new SimpleLiveRecordManager2(downloadConfig, selectedStreams, extractor); + result = await sldm.StartRecordAsync(); + } + + if (result) + { + Logger.InfoMarkUp("[white on green]Done[/]"); + } + else + { + Logger.ErrorMarkUp("[white on red]Failed[/]"); + Environment.ExitCode = 1; + } + } + + /// + /// 为批量下载生成唯一的文件名 + /// + /// 源URL + /// 批量下载索引(从1开始) + /// 总批量数 + /// 唯一文件名 + static string GetUniqueFileNameFromUrl(string url, int batchIndex, int totalBatches) + { + try + { + // 从URL提取基础名称 + var uri = new Uri(url.Split('?').First()); + var baseName = Path.GetFileNameWithoutExtension(uri.LocalPath); + + // 清理文件名,移除特殊字符 + baseName = GetValidFileName(baseName); + + // 如果基础名称为空或只有扩展名,使用URL主机名 + if (string.IsNullOrWhiteSpace(baseName) || baseName == ".m3u8" || baseName == ".mpd") + { + baseName = uri.Host.Replace(".", "_"); + } + + // 生成时间戳(精确到秒,避免同一秒内的文件名冲突) + var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + + // 生成序号部分 + var indexInfo = totalBatches > 1 ? $"_batch{batchIndex:00}_of_{totalBatches:00}" : "_batch"; + + // 生成最终文件名 + var finalName = $"{baseName}{indexInfo}_{timestamp}"; + + return finalName; + } + catch (Exception ex) + { + Logger.Warn($"[BatchDownload] Failed to generate unique filename for URL: {ex.Message}"); + // 如果解析失败,使用简单的备用名称 + var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"); + return $"batch_item_{batchIndex:00}_{timestamp}"; + } + } + + /// + /// 清理文件名中的无效字符 + /// + static string GetValidFileName(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + return "unnamed"; + + var invalidChars = Path.GetInvalidFileNameChars(); + var validName = new string(fileName.Where(c => !invalidChars.Contains(c)).ToArray()); + + // 移除连续的下划线和空格 + validName = System.Text.RegularExpressions.Regex.Replace(validName, @"_{2,}", "_"); + validName = System.Text.RegularExpressions.Regex.Replace(validName, @"\s+", "_"); + + // 限制长度 + if (validName.Length > 100) + validName = validName.Substring(0, 100); + + return validName.Trim('_'); + } + static async Task CheckUpdateAsync() { try diff --git a/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin.cs b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin.cs new file mode 100644 index 00000000..f134696b --- /dev/null +++ b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin.cs @@ -0,0 +1,730 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +// 使用System.Text.Json进行JSON配置文件解析,相比手动字符串解析更可靠 +using System.Text.Json; +using System.Threading.Tasks; +using N_m3u8DL_RE.Common.Log; + +namespace N_m3u8DL_RE.Plugin +{ + public class BatchDownloadPlugin : IPlugin + { + // 存储待下载的URL列表 + private List _urlList = new List(); + // 当前下载任务的索引,用于跟踪批量下载进度 + private int _currentIndex = 0; + // 默认输入文件路径,相对路径,相对于程序运行目录 + private string _batchFile = "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt"; + // 默认输出目录路径,相对路径,相对于程序运行目录 + // 用于存储批量下载的文件输出 + private string _outputDirectory = "extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin-output"; + // 是否为每个URL创建独立的子目录 + private bool _createSubdirectories = false; + + /// + /// 插件初始化方法,在插件被加载时由PluginManager调用 + /// + /// 执行流程: + /// 1. 首先调用ReadConfigAndCreateDirectories()从PluginConfig.json读取配置并创建必要的目录结构 + /// 这样可以确保在读取URL列表之前,输入输出目录已经存在 + /// 2. 检查插件是否在配置中启用 + /// 3. 如果启用,读取各项配置并加载URL列表 + /// + /// 为什么在ExtractEnabledFromConfig之前调用ReadConfigAndCreateDirectories? + /// 因为: + /// - 插件可能处于启用状态,但输入输出目录可能不存在 + /// - 如果目录不存在,后续的EnsureInputOutputPathsExist会尝试创建 + /// - 但如果插件未启用,我们仍然需要确保目录存在(以便用户手动添加URL) + /// - 这样设计确保无论插件是否启用,目录结构都是完整的 + /// + /// 插件配置对象(可选,当前版本从PluginConfig.json读取配置) + public void Initialize(PluginConfig? config) + { + // 【关键步骤】先读取配置并创建目录,确保后续操作有可靠的目录环境 + ReadConfigAndCreateDirectories(); + + // 从PluginConfig.json读取Enabled配置 + bool enabled = ExtractEnabledFromConfig(); + + if (enabled) + { + // 从PluginConfig.json读取BatchFile配置(输入文件路径) + _batchFile = ExtractBatchFileFromConfig(); + // 从PluginConfig.json读取OutputDirectory配置(输出目录路径) + _outputDirectory = ExtractOutputDirectoryFromConfig(); + // 从PluginConfig.json读取CreateSubdirectories配置 + _createSubdirectories = ExtractCreateSubdirectoriesFromConfig(); + + // 双重确保:再次确认输入输出路径存在 + // 虽然ReadConfigAndCreateDirectories已经创建过,但可能存在配置被修改的情况 + EnsureInputOutputPathsExist(_batchFile, _outputDirectory); + + // 记录使用的输出目录 + if (!string.IsNullOrEmpty(_outputDirectory) && Directory.Exists(_outputDirectory)) + { + Logger.Info($"[BatchDownloadPlugin] Using configured output directory: {_outputDirectory}"); + } + + // 加载URL列表 + LoadUrlList(); + Logger.Info($"[BatchDownloadPlugin] Loaded {_urlList.Count} URLs from {_batchFile}"); + } + } + + /// + /// 从PluginConfig.json读取配置并创建必要的目录结构 + /// + /// 【为什么需要这个方法?】 + /// + /// 问题背景: + /// - 插件系统通过反射动态加载,静态构造函数无法可靠执行 + /// - 编译后的输出目录可能缺少input-batch-urls.txt和BatchDownloadPlugin-output目录 + /// - 用户第一次运行程序时,如果目录不存在,插件会因找不到输入文件而失败 + /// + /// 解决方案: + /// - 在Initialize方法中,首先调用此方法读取配置并创建目录 + /// - 使用System.Text.Json解析配置文件(比手动字符串解析更可靠) + /// - 从BatchDownload配置节中读取BatchFile和OutputDirectory + /// - 确保输入文件所在目录存在,如果文件不存在则创建默认模板 + /// - 确保输出目录存在 + /// + /// 【执行步骤】 + /// 1. 构建PluginConfig.json的相对路径(extend/PluginConfig.json) + /// 2. 检查配置文件是否存在 + /// 3. 使用JsonDocument.Parse解析JSON(比手动字符串解析更可靠,避免边界条件错误) + /// 4. 获取BatchDownload配置节 + /// 5. 读取BatchFile配置,创建输入目录(如果不存在) + /// 6. 读取OutputDirectory配置,创建输出目录(如果不存在) + /// 7. 检查输入文件是否存在,如果不存在则创建默认模板文件 + /// + /// 【为什么在ExtractEnabledFromConfig之前调用?】 + /// - 即使插件未启用(Enabled=false),目录结构也应该存在 + /// - 这样用户可以手动编辑配置文件或在目录中添加文件后启用插件 + /// - 目录结构的存在是插件正常工作的前提条件 + /// + /// 【技术细节说明】 + /// - 本方法使用System.Text.Json命名空间下的JsonDocument类进行JSON解析 + /// - JsonDocument.Parse提供类型安全的JSON解析,避免手动字符串处理的边界条件问题 + /// - TryGetProperty方法用于安全地访问JSON属性,如果属性不存在不会抛出异常 + /// - GetString()方法用于获取字符串类型的配置值 + /// - 路径处理使用Path.Combine确保跨平台兼容性(Windows/Linux路径分隔符) + /// - Directory.CreateDirectory在目录已存在时不会抛出异常,安全可靠 + /// + private void ReadConfigAndCreateDirectories() + { + try + { + // 构建配置文件的相对路径 + // 相对于程序运行目录(即bin/Debug/net9.0/等输出目录) + var configPath = "extend/PluginConfig.json"; + + // 检查配置文件是否存在 + // 如果不存在,跳过目录创建步骤(可能是第一次运行或配置被删除) + if (File.Exists(configPath)) + { + // 读取配置文件内容 + var json = File.ReadAllText(configPath); + + // 使用System.Text.Json解析JSON + // 优势: + // - 内置于.NET,无需额外依赖 + // - 类型安全,避免手动字符串解析的边界条件错误 + // - 自动处理JSON结构,比IndexOf/Substring更可靠 + var configDoc = JsonDocument.Parse(json); + + // 获取BatchDownload配置节 + // TryGetProperty是安全的访问方式,如果属性不存在不会抛出异常 + if (configDoc.RootElement.TryGetProperty("BatchDownload", out var batchConfig)) + { + // 【处理BatchFile配置】 + // 从配置中读取输入文件路径 + if (batchConfig.TryGetProperty("BatchFile", out var batchFileElem)) + { + // 获取字符串值 + var batchFile = batchFileElem.GetString(); + + // 确保路径不为空 + if (!string.IsNullOrEmpty(batchFile)) + { + // 提取输入文件所在目录 + // Path.GetDirectoryName返回路径的目录部分 + // 例如:"extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt" + // 返回:"extend/BatchDownloadPlugin-and-input-output" + var inputDir = Path.GetDirectoryName(batchFile); + + if (!string.IsNullOrEmpty(inputDir)) + { + // 获取当前工作目录(程序运行目录) + var baseDir = Directory.GetCurrentDirectory(); + + // 构建完整的输入目录路径 + // Path.Combine处理不同操作系统的路径分隔符(Windows用\,Linux用/) + var fullInputDir = Path.Combine(baseDir, inputDir); + + // 检查目录是否存在 + // Directory.Exists检查文件系统,效率高且安全 + if (!Directory.Exists(fullInputDir)) + { + // 创建目录,包括所有中间目录 + // Directory.CreateDirectory如果目录已存在不会抛出异常 + Directory.CreateDirectory(fullInputDir); + Logger.Info($"[BatchDownloadPlugin] Created input directory: {fullInputDir}"); + } + } + } + } + + // 【处理OutputDirectory配置】 + // 从配置中读取输出目录路径 + if (batchConfig.TryGetProperty("OutputDirectory", out var outputDirElem)) + { + var outputDir = outputDirElem.GetString(); + + if (!string.IsNullOrEmpty(outputDir)) + { + var baseDir = Directory.GetCurrentDirectory(); + var fullOutputDir = Path.Combine(baseDir, outputDir); + + if (!Directory.Exists(fullOutputDir)) + { + Directory.CreateDirectory(fullOutputDir); + Logger.Info($"[BatchDownloadPlugin] Created output directory: {fullOutputDir}"); + } + } + } + + // 【创建默认输入文件】 + // 如果输入文件不存在,创建一个包含示例和说明的模板文件 + // 这样用户可以立即了解文件格式并添加自己的URL + if (batchConfig.TryGetProperty("BatchFile", out var batchFileForCreate)) + { + var batchFilePath = batchFileForCreate.GetString(); + + if (!string.IsNullOrEmpty(batchFilePath)) + { + var baseDir = Directory.GetCurrentDirectory(); + var fullBatchFilePath = Path.Combine(baseDir, batchFilePath); + + if (!File.Exists(fullBatchFilePath)) + { + CreateDefaultInputFile(fullBatchFilePath); + } + } + } + } + } + } + catch (Exception ex) + { + // 记录警告日志但不抛出异常 + // 这样即使配置解析失败,程序仍可继续运行(可能使用默认值) + Logger.Warn($"[BatchDownloadPlugin] Failed to read config and create directories: {ex.Message}"); + } + } + + public void OnFileDownloaded(string filePath, int downloadCount) + { + // 批量下载插件的主要逻辑在程序启动时处理 + // 此方法用于处理单个下载完成后的回调(可选) + } + + // 新增接口方法 - 提供空实现以保持向后兼容 + public void OnInputReceived(object args, object option) + { + // 【输入拦截】由PluginManager.cs统一调用此方法 + // 当用户在命令行中输入--batch等参数时,此方法会被PluginManager.NotifyPluginsOnInput调用 + Console.WriteLine($"[BatchDownloadPlugin] ✅ OnInputReceived被调用 - 参数: {args}, 选项: {option}"); + + // 检查是否包含--batch参数 + if (args is string[] argsArray) + { + var hasBatchParam = argsArray.Contains("--batch"); + Console.WriteLine($"[BatchDownloadPlugin] 检测到--batch参数: {hasBatchParam}"); + + if (hasBatchParam && HasUrls()) + { + Console.WriteLine($"[BatchDownloadPlugin] ✅ 检测到批量下载模式,URL列表包含 {_urlList.Count} 个条目"); + } + } + } + + public void OnOutputGenerated(string outputPath, string outputType) + { + // 空实现 - 可在后续阶段中扩展 + } + + public void OnLogGenerated(string logMessage, PluginLogLevel logLevel) + { + // 空实现 - 可在后续阶段中扩展 + } + + /// + /// 从批量下载输入文件中加载URL列表 + /// + /// 【为什么需要这个方法?】 + /// + /// 1. 读取配置文件中指定的URL列表文件 + /// 2. 解析文件内容,提取有效的URL + /// 3. 过滤掉注释行和空行 + /// 4. 存储到_urlList字段供后续使用 + /// + /// 【文件格式要求】 + /// - 每行一个URL + /// - 以#开头的行被视为注释,被跳过 + /// - 空行被跳过 + /// - 支持任意有效的URL格式 + /// + /// 【执行步骤】 + /// 1. 检查文件是否存在 + /// 2. 读取所有行 + /// 3. 对每行进行修剪(去除首尾空白) + /// 4. 跳过空行和注释行 + /// 5. 将有效URL添加到_urlList + /// + /// 【错误处理】 + /// - 如果文件不存在,记录警告日志 + /// - 不抛出异常,允许程序继续运行 + /// + /// 【使用示例】 + /// # 这是注释,会被跳过 + /// https://example.com/video1.m3u8 + /// https://example.com/video2.m3u8 + /// + /// 【注意事项】 + /// - URL会被原样存储,不进行验证 + /// - URL的验证在实际下载时进行 + /// - 大文件可能导致内存占用增加 + /// + private void LoadUrlList() + { + if (File.Exists(_batchFile)) + { + var lines = File.ReadAllLines(_batchFile); + foreach (var line in lines) + { + var trimmedLine = line.Trim(); + if (!string.IsNullOrEmpty(trimmedLine) && !trimmedLine.StartsWith("#")) + { + _urlList.Add(trimmedLine); + } + } + } + else + { + Logger.Warn($"[BatchDownloadPlugin] Batch file not found: {_batchFile}"); + } + } + + /// + /// 从PluginConfig.json的BatchDownload配置节中提取BatchFile配置值 + /// + /// 【为什么需要这个方法?】 + /// + /// 由于以下原因,需要单独的方法从配置中提取BatchFile: + /// 1. ReadConfigAndCreateDirectories使用System.Text.Json进行配置解析 + /// 2. 但其他Extract方法使用手动字符串解析(IndexOf/Substring) + /// 3. 这种设计是为了演示不同的配置解析方式 + /// + /// 【解析方法说明】 + /// - 使用IndexOf定位"BatchDownload"配置节的开头 + /// - 使用IndexOf定位该节的结束括号(第一个}) + /// - 在节内查找"BatchFile"字段 + /// - 提取冒号后的值,直到逗号或右括号 + /// - 去除首尾的引号 + /// + /// 【注意】 + /// 这种手动字符串解析方式不够健壮,仅作为示例 + /// 实际项目中建议统一使用System.Text.Json进行配置解析 + /// + /// 【返回值】 + /// - 如果配置有效,返回配置中指定的BatchFile路径 + /// - 如果配置无效或解析失败,返回默认值 + /// + /// BatchFile配置值(相对路径) + private string ExtractBatchFileFromConfig() + { + try + { + var configPath = "extend/PluginConfig.json"; + if (File.Exists(configPath)) + { + var json = File.ReadAllText(configPath); + + // 查找"BatchDownload"部分 + var batchStart = json.IndexOf("\"BatchDownload\""); + if (batchStart == -1) return "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt"; + + // 查找BatchDownload部分的结束位置 + var batchEnd = json.IndexOf("}", batchStart); + if (batchEnd == -1) return "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt"; + + // 在BatchDownload部分内查找"BatchFile"字段 + var batchFileStart = json.IndexOf("\"BatchFile\"", batchStart, batchEnd - batchStart); + if (batchFileStart == -1) return "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt"; + + var valueStart = json.IndexOf(":", batchFileStart) + 1; + var valueEnd = json.IndexOf(",", valueStart); + if (valueEnd == -1 || valueEnd > batchEnd) valueEnd = json.IndexOf("}", valueStart); + + if (valueStart > 0 && valueEnd > valueStart) + { + var batchFileValue = json.Substring(valueStart, valueEnd - valueStart).Trim(); + batchFileValue = batchFileValue.Trim('"'); + return batchFileValue; + } + } + } + catch (Exception ex) + { + Logger.Warn($"[BatchDownloadPlugin] Failed to extract BatchFile from config: {ex.Message}"); + } + + return "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt"; // 默认值 + } + + /// + /// 从PluginConfig.json的BatchDownload配置节中提取Enabled配置值 + /// + /// 【为什么需要这个方法?】 + /// + /// 1. 插件的启用状态需要独立判断 + /// 2. 在Initialize方法中,需要先判断是否启用再执行其他逻辑 + /// 3. 默认情况下插件是禁用的,避免对用户造成干扰 + /// + /// 【解析方法说明】 + /// - 使用IndexOf定位"BatchDownload"配置节 + /// - 在节内使用IndexOf查找"Enabled"字段 + /// - 提取冒号后的布尔值(true/false) + /// - 使用StringComparison.OrdinalIgnoreCase进行不区分大小写的比较 + /// + /// 【配置示例】 + /// "BatchDownload": { + /// "Enabled": true, // 插件启用开关 + /// ... + /// } + /// + /// 【返回值】 + /// - true: 插件已启用,可以执行批量下载 + /// - false: 插件未启用,跳过批量下载逻辑 + /// + /// 是否启用插件 + private bool ExtractEnabledFromConfig() + { + try + { + var configPath = "extend/PluginConfig.json"; + if (File.Exists(configPath)) + { + var json = File.ReadAllText(configPath); + + // 查找"BatchDownload"部分 + var batchStart = json.IndexOf("\"BatchDownload\""); + if (batchStart == -1) return false; + + // 查找BatchDownload部分的结束位置 + var batchEnd = json.IndexOf("}", batchStart); + if (batchEnd == -1) return false; + + // 在BatchDownload部分内查找"Enabled"字段 + var enabledStart = json.IndexOf("\"Enabled\"", batchStart, batchEnd - batchStart); + if (enabledStart == -1) return false; + + var valueStart = json.IndexOf(":", enabledStart) + 1; + var valueEnd = json.IndexOf(",", valueStart); + if (valueEnd == -1 || valueEnd > batchEnd) valueEnd = json.IndexOf("}", valueStart); + + if (valueStart > 0 && valueEnd > valueStart) + { + var enabledValue = json.Substring(valueStart, valueEnd - valueStart).Trim(); + return enabledValue.Equals("true", StringComparison.OrdinalIgnoreCase); + } + } + } + catch (Exception ex) + { + Logger.Warn($"[BatchDownloadPlugin] Failed to extract Enabled from config: {ex.Message}"); + } + + return false; // 默认值 + } + + /// + /// 从PluginConfig.json的BatchDownload配置节中提取CreateSubdirectories配置值 + /// + /// 【为什么需要这个方法?】 + /// + /// 1. 控制批量下载时的目录创建行为 + /// 2. 如果设置为true,每个URL会创建独立的子目录 + /// 3. 如果设置为false,所有文件直接下载到输出目录 + /// 4. 默认值为false,避免创建过多嵌套目录 + /// + /// 【配置示例】 + /// "BatchDownload": { + /// "CreateSubdirectories": true, // 是否为每个URL创建子目录 + /// ... + /// } + /// + /// 【使用场景】 + /// - true: 当多个视频有相同文件名时,避免覆盖 + /// - false: 当只需要简单的目录结构时 + /// + /// 【返回值】 + /// - true: 为每个URL创建子目录 + /// - false: 不创建子目录,所有文件在同一目录 + /// + /// 是否创建子目录 + private bool ExtractCreateSubdirectoriesFromConfig() + { + try + { + var configPath = "extend/PluginConfig.json"; + if (File.Exists(configPath)) + { + var json = File.ReadAllText(configPath); + + // 查找"BatchDownload"部分 + var batchStart = json.IndexOf("\"BatchDownload\""); + if (batchStart == -1) return false; + + // 查找BatchDownload部分的结束位置 + var batchEnd = json.IndexOf("}", batchStart); + if (batchEnd == -1) return false; + + // 在BatchDownload部分内查找"CreateSubdirectories"字段 + var createSubdirsStart = json.IndexOf("\"CreateSubdirectories\"", batchStart, batchEnd - batchStart); + if (createSubdirsStart == -1) return false; + + var valueStart = json.IndexOf(":", createSubdirsStart) + 1; + var valueEnd = json.IndexOf(",", valueStart); + if (valueEnd == -1 || valueEnd > batchEnd) valueEnd = json.IndexOf("}", valueStart); + + if (valueStart > 0 && valueEnd > valueStart) + { + var createSubdirsValue = json.Substring(valueStart, valueEnd - valueStart).Trim(); + return createSubdirsValue.Equals("true", StringComparison.OrdinalIgnoreCase); + } + } + } + catch (Exception ex) + { + Logger.Warn($"[BatchDownloadPlugin] Failed to extract CreateSubdirectories from config: {ex.Message}"); + } + + return false; // 默认值 + } + + /// + /// 从PluginConfig.json的BatchDownload配置节中提取OutputDirectory配置值 + /// + /// 【为什么需要这个方法?】 + /// + /// 1. 指定批量下载文件的输出目录 + /// 2. 允许用户自定义输出位置 + /// 3. 如果配置无效或未配置,返回null,由调用方使用默认值 + /// + /// 【解析方法说明】 + /// - 使用IndexOf定位"BatchDownload"配置节 + /// - 在节内使用IndexOf查找"OutputDirectory"字段 + /// - 提取冒号后的字符串值(路径) + /// - 去除首尾的引号 + /// + /// 【配置示例】 + /// "BatchDownload": { + /// "OutputDirectory": "extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin-output", + /// ... + /// } + /// + /// 【返回值】 + /// - 配置的输出目录路径(相对路径) + /// - null: 配置无效或未配置 + /// + /// 【路径处理】 + /// - 返回的是相对路径(相对于程序运行目录) + /// - 实际使用时需要与Directory.GetCurrentDirectory()组合 + /// + /// OutputDirectory配置值(相对路径) + private string ExtractOutputDirectoryFromConfig() + { + try + { + var configPath = "extend/PluginConfig.json"; + if (File.Exists(configPath)) + { + var json = File.ReadAllText(configPath); + + // 查找"BatchDownload"部分 + var batchStart = json.IndexOf("\"BatchDownload\""); + if (batchStart == -1) return null; + + // 查找BatchDownload部分的结束位置 + var batchEnd = json.IndexOf("}", batchStart); + if (batchEnd == -1) return null; + + // 在BatchDownload部分内查找"OutputDirectory"字段 + var outputDirStart = json.IndexOf("\"OutputDirectory\"", batchStart, batchEnd - batchStart); + if (outputDirStart == -1) return null; + + var valueStart = json.IndexOf(":", outputDirStart) + 1; + var valueEnd = json.IndexOf(",", valueStart); + if (valueEnd == -1 || valueEnd > batchEnd) valueEnd = json.IndexOf("}", valueStart); + + if (valueStart > 0 && valueEnd > valueStart) + { + var outputDirValue = json.Substring(valueStart, valueEnd - valueStart).Trim(); + outputDirValue = outputDirValue.Trim('"'); + return outputDirValue; + } + } + } + catch (Exception ex) + { + Logger.Warn($"[BatchDownloadPlugin] Failed to extract OutputDirectory from config: {ex.Message}"); + } + + return null; // 默认值 + } + + /// + /// 确保输入文件和输出目录存在,如果不存在则创建 + /// + /// 【为什么需要这个方法?】 + /// + /// 在ReadConfigAndCreateDirectories方法之后再次调用此方法的原因: + /// 1. 配置可能在ReadConfigAndCreateDirectories之后被用户修改 + /// 2. 确保路径变更后仍然能正确创建目录结构 + /// 3. 双重检查机制提高可靠性 + /// + /// 【执行步骤】 + /// 1. 获取当前工作目录(程序运行目录) + /// 2. 构建输入文件的完整路径 + /// 3. 创建输入文件所在目录(如果不存在) + /// 4. 如果输入文件不存在,创建默认模板文件 + /// 5. 构建输出目录的完整路径 + /// 6. 创建输出目录(如果不存在) + /// + /// 【路径处理说明】 + /// - 使用Directory.GetCurrentDirectory()获取程序运行目录 + /// - 使用Path.Combine()组合路径,兼容不同操作系统的路径分隔符 + /// - 使用Path.GetDirectoryName()提取目录部分 + /// + /// 【异常处理】 + /// - 所有文件操作都包装在try-catch块中 + /// - 即使创建失败也只记录警告日志,不抛出异常 + /// - 这样可以保证程序的其余部分继续正常运行 + /// + /// 输入文件路径(相对路径) + /// 输出目录路径(相对路径) + private void EnsureInputOutputPathsExist(string inputFile, string outputDirectory) + { + try + { + // 获取当前工作目录,兼容编译后的环境 + var baseDir = Directory.GetCurrentDirectory(); + + // 确保输入文件路径存在 + var inputPath = Path.Combine(baseDir, inputFile); + var inputDir = Path.GetDirectoryName(inputPath); + + if (!string.IsNullOrEmpty(inputDir) && !Directory.Exists(inputDir)) + { + Directory.CreateDirectory(inputDir); + Logger.Info($"[BatchDownloadPlugin] Created input directory: {inputDir}"); + } + + // 创建默认的输入文件(如果不存在) + if (!File.Exists(inputPath)) + { + CreateDefaultInputFile(inputPath); + } + + // 确保输出目录存在 + if (!string.IsNullOrEmpty(outputDirectory)) + { + var outputPath = Path.Combine(baseDir, outputDirectory); + if (!Directory.Exists(outputPath)) + { + Directory.CreateDirectory(outputPath); + Logger.Info($"[BatchDownloadPlugin] Created output directory: {outputPath}"); + } + } + + Logger.Info($"[BatchDownloadPlugin] Input/Output paths initialized successfully"); + } + catch (Exception ex) + { + Logger.Warn($"[BatchDownloadPlugin] Failed to ensure paths exist: {ex.Message}"); + } + } + + /// + /// 创建默认的批量下载输入文件模板 + /// + /// 【为什么需要这个方法?】 + /// + /// 当用户第一次运行程序或输入文件被删除时: + /// 1. 需要提供一个模板文件,让用户了解文件格式 + /// 2. 模板文件包含示例URL和注释说明 + /// 3. 用户只需删除示例并添加自己的URL即可使用 + /// + /// 【模板文件格式】 + /// - 以#开头的行为注释,会被LoadUrlList跳过 + /// - 空行会被跳过 + /// - 每行一个URL,可以是m3u8视频流地址 + /// + /// 【执行步骤】 + /// 1. 定义默认的模板内容(包含说明和示例) + /// 2. 提取输入文件的目录路径 + /// 3. 如果目录不存在则创建目录 + /// 4. 将模板内容写入文件 + /// + /// 【使用场景】 + /// - 程序首次运行,input-batch-urls.txt不存在时 + /// - 用户误删输入文件后重新生成 + /// - 提供给新用户了解文件格式 + /// + /// 【注意事项】 + /// - 不会覆盖已存在的文件 + /// - 文件编码使用UTF-8,支持中文内容 + /// + /// 要创建的输入文件完整路径 + private void CreateDefaultInputFile(string inputPath) + { + try + { + var defaultContent = @"# 批量下载URL列表 +# 注释行以#开头 +# 请在此处添加要下载的m3u8 URL,每行一个 +# 例如: +# https://example.com/video1.m3u8 +# https://example.com/video2.m3u8 + +"; + + var dir = Path.GetDirectoryName(inputPath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + File.WriteAllText(inputPath, defaultContent); + Logger.Info($"[BatchDownloadPlugin] Created default input file: {inputPath}"); + } + catch (Exception ex) + { + Logger.Warn($"[BatchDownloadPlugin] Failed to create default input file: {ex.Message}"); + } + } + + public List GetUrlList() => _urlList; + public bool HasUrls() => _urlList.Count > 0; + + // 返回输出目录配置 + public string GetOutputDirectory() => _outputDirectory ?? ExtractOutputDirectoryFromConfig(); + + // 返回简化的配置对象,只包含实际使用的属性 + public object GetConfig() => new { CreateSubdirectories = _createSubdirectories }; + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/ProxySwitcherPlugin.cs b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/ProxySwitcherPlugin.cs new file mode 100644 index 00000000..8e2a37b9 --- /dev/null +++ b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/ProxySwitcherPlugin.cs @@ -0,0 +1,68 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using N_m3u8DL_RE.Common.Log; + +namespace N_m3u8DL_RE.Plugin +{ + public class ProxySwitcherPlugin : IPlugin + { + private PluginConfig? _config; + private int _downloadCount = 0; + private readonly HttpClient _httpClient = new HttpClient(); + + public void Initialize(PluginConfig? config) + { + _config = config; + Logger.Info("[ProxySwitcherPlugin] Initialized"); + } + + public void OnFileDownloaded(string filePath, int downloadCount) + { + _downloadCount = downloadCount; + + // 检查是否启用插件且达到切换间隔 + if (_config?.ProxySwitcher?.Enabled == true && + _downloadCount % _config.ProxySwitcher.SwitchInterval == 0) + { + SwitchProxy(); + } + } + + private async void SwitchProxy() + { + try + { + var clashApiUrl = _config?.ProxySwitcher?.ClashApiUrl ?? "http://127.0.0.1:9090"; + var proxiesResponse = await _httpClient.GetAsync($"{clashApiUrl}/proxies"); + var json = await proxiesResponse.Content.ReadAsStringAsync(); + + // 这里应该解析JSON并选择一个代理进行切换 + // 为简化示例,我们只记录日志 + Logger.Info($"[ProxySwitcherPlugin] Would switch proxy via Clash API at {clashApiUrl}"); + Logger.Debug($"[ProxySwitcherPlugin] Proxies response: {json}"); + } + catch (Exception ex) + { + Logger.Error($"[ProxySwitcherPlugin] Failed to switch proxy: {ex.Message}"); + } + } + + // 新增接口方法 - 提供空实现以保持向后兼容 + public void OnInputReceived(object args, object option) + { + // 空实现 - 可在后续阶段中扩展 + } + + public void OnOutputGenerated(string outputPath, string outputType) + { + // 空实现 - 可在后续阶段中扩展 + } + + public void OnLogGenerated(string logMessage, PluginLogLevel logLevel) + { + // 空实现 - 可在后续阶段中扩展 + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/UASwitcherPlugin.cs b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/UASwitcherPlugin.cs new file mode 100644 index 00000000..a18f2825 --- /dev/null +++ b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/UASwitcherPlugin.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using N_m3u8DL_RE.Common.Log; + +namespace N_m3u8DL_RE.Plugin +{ + public class UASwitcherPlugin : IPlugin + { + private PluginConfig? _config; + private readonly List _userAgents = new List + { + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36" + }; + private int _currentIndex = 0; + + public void Initialize(PluginConfig? config) + { + _config = config; + + // 如果配置中有自定义UA,则使用配置中的 + if (config?.UASwitcher?.UserAgents != null && config.UASwitcher.UserAgents.Count > 0) + { + _userAgents.Clear(); + _userAgents.AddRange(config.UASwitcher.UserAgents); + } + + // Convert User-Agent list to command line arguments + var headers = _userAgents.Select(ua => $"-H \"User-Agent: {ua}\"").ToList(); + Logger.Info($"[UASwitcherPlugin] Initialized with headers: {string.Join(", ", headers)}"); + } + + public void OnFileDownloaded(string filePath, int downloadCount) + { + Logger.Info($"[UASwitcherPlugin] File downloaded: {filePath}, count: {downloadCount}"); + + // 每1个文件切换一次UA(原来是每3个文件切换一次) + if (_userAgents.Count > 0) + { + string newUA = _userAgents[downloadCount % _userAgents.Count]; + Logger.Info($"[UASwitcherPlugin] Downloaded {downloadCount} files, switching UA to: {newUA}"); + + // 注意:这里只是示例输出,实际应用中需要与HTTP客户端集成 + // 可以通过修改全局HTTP客户端的默认请求头来实现 + // HttpClient.DefaultRequestHeaders.Add("User-Agent", newUA); + } + } + + // 新增接口方法 - 提供空实现以保持向后兼容 + public void OnInputReceived(object args, object option) + { + // 空实现 - 可在后续阶段中扩展 + } + + public void OnOutputGenerated(string outputPath, string outputType) + { + // 空实现 - 可在后续阶段中扩展 + } + + public void OnLogGenerated(string logMessage, PluginLogLevel logLevel) + { + // 空实现 - 可在后续阶段中扩展 + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt new file mode 100644 index 00000000..c9ee1d88 --- /dev/null +++ b/src/N_m3u8DL-RE/extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt @@ -0,0 +1 @@ +https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/hls/xgplayer-demo.m3u8 \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/Interceptors/InputStreamInterceptor.cs b/src/N_m3u8DL-RE/extend/Interceptors/InputStreamInterceptor.cs new file mode 100644 index 00000000..2413302d --- /dev/null +++ b/src/N_m3u8DL-RE/extend/Interceptors/InputStreamInterceptor.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; + +namespace N_m3u8DL_RE.Plugin +{ + /// + /// 输入流拦截器类 + /// + /// 【接管说明】此类的功能由 PluginManager.cs 接管和调用: + /// - 在 CommandInvoker.cs 的 InvokeArgs 方法开始处被调用 + /// - 插件系统通过 PluginManager.cs 统一管理和调用此拦截器 + /// - 插件的输入拦截功能由 PluginManager.NotifyPluginsOnInput() 方法协调 + /// + /// 设计目的: + /// 1. 提供参数和选项的拦截机制 + /// 2. 支持插件对输入流进行处理和修改 + /// 3. 为批量下载等高级插件功能提供输入流控制 + /// + public class InputStreamInterceptor + { + // 静态拦截器列表 - 用于存储所有注册的拦截器 + private static List _interceptors = new List(); + + /// + /// 注册拦截器 + /// 【接管说明】此方法由插件系统调用,插件管理器通过PluginManager.cs统一管理 + /// + /// + /// 要注册的拦截器实例 + public static void RegisterInterceptor(IStreamInterceptor interceptor) + { + _interceptors.Add(interceptor); + } + + /// + /// 拦截命令行参数 + /// 【接管说明】此方法由 PluginManager.cs 在 CommandInvoker.cs 中统一调用 + /// + /// 工作流程: + /// 1. PluginManager.NotifyPluginsOnInput() 触发输入事件 + /// 2. 各插件的 OnInputReceived() 方法会被调用 + /// 3. 插件可以通过 IStreamInterceptor 接口处理参数 + /// + /// 原始命令行参数数组 + /// 经过拦截器处理后的参数数组 + public static string[] InterceptArgs(string[] originalArgs) + { + var result = originalArgs; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptInput(result); + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; + } + + /// + /// 拦截选项对象 + /// 【接管说明】此方法由 PluginManager.cs 在 CommandInvoker.cs 中统一调用 + /// + /// 工作流程: + /// 1. PluginManager.NotifyPluginsOnInput() 触发输入事件 + /// 2. 各插件的 OnInputReceived() 方法会被调用 + /// 3. 插件可以通过 IStreamInterceptor 接口处理选项对象 + /// + /// 原始选项对象 + /// 经过拦截器处理后的选项对象 + public static object InterceptOptions(object originalOption) + { + var result = originalOption; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptOptions(result); + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/Interceptors/LogStreamInterceptor.cs b/src/N_m3u8DL-RE/extend/Interceptors/LogStreamInterceptor.cs new file mode 100644 index 00000000..f999652f --- /dev/null +++ b/src/N_m3u8DL-RE/extend/Interceptors/LogStreamInterceptor.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace N_m3u8DL_RE.Plugin +{ + // 【日志拦截】由PluginManager.cs统一管理日志流拦截功能 + // 该类处理Console输出重定向和日志拦截逻辑,确保日志不丢失且可被插件拦截 + + public class LogStreamInterceptor + { + private static List _interceptors = new List(); + private static StringWriter? _originalConsoleOut; + private static StringWriter? _originalConsoleError; + private static bool _isInitialized = false; + private static bool _isEnabled = false; // 新增:是否启用的标志 + + /// + /// 初始化日志拦截器,重定向Console输出 + /// 【日志拦截】由PluginManager.cs统一调用此方法进行初始化 + /// + public static void Initialize(bool enabled = false) + { + _isEnabled = enabled; + + if (_isInitialized || !_isEnabled) return; + + try + { + // 【调试】记录Console.Out的类型 + var originalOutType = Console.Out.GetType().Name; + var originalErrorType = Console.Error.GetType().Name; + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] Console.Out类型: {originalOutType}, Console.Error类型: {originalErrorType}"); + + // 先输出初始化消息到实际控制台(避免被拦截) + Console.WriteLine("[LogInterceptor] 日志流拦截器初始化开始..."); + + // 保存原始Console引用 + _originalConsoleOut = new StringWriter(); + _originalConsoleError = new StringWriter(); + + // 复制当前的Console内容到StringWriter + var currentOut = Console.Out; + var currentError = Console.Error; + + // 创建拦截的StringWriter + var interceptedOut = new InterceptedStringWriter(_originalConsoleOut, "stdout"); + var interceptedErr = new InterceptedStringWriter(_originalConsoleError, "stderr"); + + // 输出初始化完成消息到实际控制台 + Console.WriteLine("[LogInterceptor] 日志流拦截器初始化完成"); + + _isInitialized = true; + + // 重定向Console输出 + Console.SetOut(interceptedOut); + Console.SetError(interceptedErr); + + System.Diagnostics.Debug.WriteLine("[LogInterceptor] 日志流拦截器已启用并重定向Console输出"); + } + catch (Exception ex) + { + // 使用Debug输出错误信息,避免Console重定向问题 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 异常详情: {ex.StackTrace}"); + + try + { + // 尝试使用Console.WriteLine输出错误 + Console.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + } + catch + { + // 如果Console不可用,忽略 + } + } + } + + /// + /// 注册日志拦截器 + /// 【日志拦截】由PluginManager.cs统一管理拦截器注册 + /// + public static void RegisterInterceptor(IStreamInterceptor interceptor) + { + if (!_isEnabled) + { + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 拦截器未启用,跳过注册: {interceptor.GetType().Name}"); + return; + } + + if (!_interceptors.Contains(interceptor)) + { + _interceptors.Add(interceptor); + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 已注册拦截器: {interceptor.GetType().Name}"); + } + } + + /// + /// 拦截并处理日志消息 + /// 【日志拦截】由PluginManager.cs统一调用此方法进行日志拦截处理 + /// + public static string InterceptLog(string originalLog, PluginLogLevel level) + { + if (!_isEnabled) + return originalLog; + + if (string.IsNullOrEmpty(originalLog)) + return originalLog; + + var result = originalLog; + + // 依次调用所有拦截器进行处理 + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptLog(result, level); + if (string.IsNullOrEmpty(result)) + result = originalLog; // 如果被拦截为空,保留原始日志 + } + catch (Exception ex) + { + _originalConsoleOut?.WriteLine($"[LogInterceptor] 拦截器 {interceptor.GetType().Name} 处理错误: {ex.Message}"); + } + } + + return result; + } + + /// + /// 恢复原始Console输出 + /// 【日志拦截】由PluginManager.cs统一调用此方法进行清理 + /// + public static void Restore() + { + if (!_isEnabled || !_isInitialized) + return; + + try + { + if (_originalConsoleOut != null) + Console.SetOut(_originalConsoleOut); + + if (_originalConsoleError != null) + Console.SetError(_originalConsoleError); + + _isInitialized = false; + Console.WriteLine("[LogInterceptor] 日志流拦截器已恢复"); + + System.Diagnostics.Debug.WriteLine("[LogInterceptor] 日志流拦截器已禁用并恢复Console输出"); + } + catch (Exception ex) + { + Console.WriteLine($"[LogInterceptor] 恢复失败: {ex.Message}"); + } + } + + /// + /// 获取拦截器列表(用于调试) + /// 【日志拦截】由PluginManager.cs统一管理拦截器状态查询 + /// + public static List GetInterceptors() + { + return new List(_interceptors); + } + } + + /// + /// 拦截的StringWriter实现 + /// 【日志拦截】由PluginManager.cs统一创建和管理此类的实例 + /// + public class InterceptedStringWriter : StringWriter + { + private readonly StringWriter _original; + private readonly string _streamType; + + public InterceptedStringWriter(StringWriter original, string streamType) + { + _original = original ?? throw new ArgumentNullException(nameof(original)); + _streamType = streamType ?? throw new ArgumentNullException(nameof(streamType)); + } + + public override void Write(string? value) + { + if (!string.IsNullOrEmpty(value)) + { + var intercepted = LogStreamInterceptor.InterceptLog(value, PluginLogLevel.Info); + + // 输出到原始Console + _original?.Write(intercepted); + + // 同时输出到此StringWriter以保持功能 + base.Write(intercepted); + } + } + + public override void WriteLine(string? value) + { + if (!string.IsNullOrEmpty(value)) + { + var intercepted = LogStreamInterceptor.InterceptLog(value, PluginLogLevel.Info); + + // 输出到原始Console + _original?.WriteLine(intercepted); + + // 同时输出到此StringWriter以保持功能 + base.WriteLine(intercepted); + } + } + + public override void WriteLine() + { + _original?.WriteLine(); + base.WriteLine(); + } + + public override Encoding Encoding => _original?.Encoding ?? Encoding.UTF8; + + public override string ToString() + { + return base.ToString(); + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/Interceptors/OutputStreamInterceptor.cs b/src/N_m3u8DL-RE/extend/Interceptors/OutputStreamInterceptor.cs new file mode 100644 index 00000000..7950ed23 --- /dev/null +++ b/src/N_m3u8DL-RE/extend/Interceptors/OutputStreamInterceptor.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace N_m3u8DL_RE.Plugin +{ + // 【输出流拦截】由PluginManager.cs统一管理输出流拦截功能 + // 该类处理输出流拦截和重定向逻辑,确保输出不丢失且可被插件拦截 + + public class OutputStreamInterceptor + { + private static List _interceptors = new List(); + private static StringWriter? _originalOutput; + private static bool _isInitialized = false; + + /// + /// 初始化输出流拦截器 + /// 【输出流拦截】由PluginManager.cs统一调用此方法进行初始化 + /// + public static void Initialize() + { + if (_isInitialized) return; + + try + { + // 先输出初始化消息到实际控制台(避免被拦截) + Console.WriteLine("[OutputInterceptor] 输出流拦截器初始化开始..."); + + // 保存原始输出引用 + _originalOutput = new StringWriter(); + + _isInitialized = true; + + // 输出初始化完成消息到实际控制台 + Console.WriteLine("[OutputInterceptor] 输出流拦截器初始化完成"); + } + catch (Exception ex) + { + // 使用Debug输出错误信息 + System.Diagnostics.Debug.WriteLine($"[OutputInterceptor] 初始化失败: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"[OutputInterceptor] 异常详情: {ex.StackTrace}"); + } + } + + /// + /// 注册输出流拦截器 + /// 【输出流拦截】由PluginManager.cs统一管理拦截器注册 + /// + public static void RegisterInterceptor(IStreamInterceptor interceptor) + { + if (!_interceptors.Contains(interceptor)) + { + _interceptors.Add(interceptor); + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[OutputInterceptor] 已注册输出流拦截器: {interceptor.GetType().Name}"); + } + } + + /// + /// 拦截并处理输出消息 + /// 【输出流拦截】由PluginManager.cs统一调用此方法进行输出拦截处理 + /// + public static string InterceptOutput(string originalOutput, string outputType) + { + if (string.IsNullOrEmpty(originalOutput)) + return originalOutput; + + var result = originalOutput; + + // 依次调用所有拦截器进行处理 + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptOutput(result, outputType); + if (string.IsNullOrEmpty(result)) + result = originalOutput; // 如果被拦截为空,保留原始输出 + } + catch (Exception ex) + { + _originalOutput?.WriteLine($"[OutputInterceptor] 拦截器 {interceptor.GetType().Name} 处理错误: {ex.Message}"); + } + } + + return result; + } + + /// + /// 处理输出重定向事件 + /// 【输出流拦截】由PluginManager.cs统一调用此方法处理输出重定向 + /// + public static void OnOutputRedirect(string originalPath, string newPath) + { + foreach (var interceptor in _interceptors) + { + try + { + interceptor.OnOutputRedirect(originalPath, newPath); + } + catch (Exception ex) + { + _originalOutput?.WriteLine($"[OutputInterceptor] 拦截器 {interceptor.GetType().Name} 输出重定向处理错误: {ex.Message}"); + } + } + } + + /// + /// 恢复原始输出流 + /// 【输出流拦截】由PluginManager.cs统一调用此方法进行清理 + /// + public static void Restore() + { + try + { + if (_originalOutput != null) + Console.SetOut(_originalOutput); + + _isInitialized = false; + Console.WriteLine("[OutputInterceptor] 输出流拦截器已恢复"); + } + catch (Exception ex) + { + Console.WriteLine($"[OutputInterceptor] 恢复失败: {ex.Message}"); + } + } + + /// + /// 获取拦截器列表(用于调试) + /// 【输出流拦截】由PluginManager.cs统一管理拦截器状态查询 + /// + public static List GetInterceptors() + { + return new List(_interceptors); + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/Interceptors/StreamInterceptorPlugin.cs b/src/N_m3u8DL-RE/extend/Interceptors/StreamInterceptorPlugin.cs new file mode 100644 index 00000000..0514635b --- /dev/null +++ b/src/N_m3u8DL-RE/extend/Interceptors/StreamInterceptorPlugin.cs @@ -0,0 +1,80 @@ +using System; +using System.IO; + +namespace N_m3u8DL_RE.Plugin +{ + // 【流拦截器插件】由PluginManager.cs统一管理此插件的注册和调用 + // 该插件演示如何实现IStreamInterceptor接口以进行流拦截操作 + + public class StreamInterceptorPlugin : IPlugin, IStreamInterceptor + { + private PluginConfig? _config; + + public void Initialize(PluginConfig? config) + { + _config = config; + Console.WriteLine("[StreamInterceptorPlugin] Initialized"); + } + + public void OnFileDownloaded(string filePath, int downloadCount) + { + Console.WriteLine($"[StreamInterceptorPlugin] File downloaded: {filePath} (count: {downloadCount})"); + } + + // IPlugin 新接口实现 + public void OnInputReceived(object args, object option) + { + Console.WriteLine($"[StreamInterceptorPlugin] Input received"); + } + + public void OnOutputGenerated(string outputPath, string outputType) + { + Console.WriteLine($"[StreamInterceptorPlugin] Output generated: {outputPath} (type: {outputType})"); + } + + public void OnLogGenerated(string logMessage, PluginLogLevel logLevel) + { + Console.WriteLine($"[StreamInterceptorPlugin] Log generated: [{logLevel}] {logMessage}"); + } + + // IStreamInterceptor 接口实现 + + // 输入流拦截 + public string[] InterceptInput(string[] originalArgs) + { + Console.WriteLine($"[StreamInterceptorPlugin] Intercepting input: {originalArgs.Length} arguments"); + return originalArgs; // 原样返回,实际实现中可以修改参数 + } + + public object InterceptOptions(object originalOption) + { + Console.WriteLine("[StreamInterceptorPlugin] Intercepting options"); + return originalOption; // 原样返回,实际实现中可以修改选项 + } + + // 输出流拦截 + public string InterceptOutput(string originalOutput, string outputType) + { + Console.WriteLine($"[StreamInterceptorPlugin] Intercepting output: {outputType}"); + return originalOutput; // 原样返回,实际实现中可以修改输出 + } + + public void OnOutputRedirect(string originalPath, string newPath) + { + Console.WriteLine($"[StreamInterceptorPlugin] Output redirected from {originalPath} to {newPath}"); + } + + // 日志流拦截 + public string InterceptLog(string originalLog, PluginLogLevel level) + { + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[StreamInterceptorPlugin] Intercepting log: {level}"); + return originalLog; // 原样返回,实际实现中可以修改日志 + } + + public void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination) + { + Console.WriteLine($"[StreamInterceptorPlugin] Log redirected to {newDestination}: {level}"); + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/PluginConfig.json b/src/N_m3u8DL-RE/extend/PluginConfig.json new file mode 100644 index 00000000..f2f93891 --- /dev/null +++ b/src/N_m3u8DL-RE/extend/PluginConfig.json @@ -0,0 +1,28 @@ +{ + "UASwitcher": { + "Enabled": false, + "UserAgents": [ + "自定义 User-Agent 1", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36" + ] + }, + "ProxySwitcher": { + "Enabled": false, + "ClashApiUrl": "http://127.0.0.1:9090", + "SwitchInterval": 1 + }, + "BatchDownload": { + "Enabled": true, + "BatchFile": "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt", + "MaxConcurrentDownloads": 1, + "OutputDirectory": "extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin-output", + "RetryCount": 3, + "CreateSubdirectories": false + }, + "StreamInterceptor": { + "Enabled": false, + "InterceptLevel": "Info", + "LogDestination": "Console" + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/PluginManager.cs b/src/N_m3u8DL-RE/extend/PluginManager.cs new file mode 100644 index 00000000..93b27860 --- /dev/null +++ b/src/N_m3u8DL-RE/extend/PluginManager.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using N_m3u8DL_RE.Common.Entity; + +namespace N_m3u8DL_RE.Plugin +{ + public enum PluginLogLevel + { + Debug, + Info, + Warn, + Error, + Fatal + } + + public static class PluginManager + { + private static readonly List _plugins = new List(); + private static readonly List _streamInterceptors = new List(); + private static int _downloadCount = 0; + private static PluginConfig? _config; + + // 【插件管理】由PluginManager.cs统一管理插件系统核心功能 + + // 新增流拦截器管理方法 + public static void RegisterStreamInterceptor(IStreamInterceptor interceptor) + { + if (!_streamInterceptors.Contains(interceptor)) + { + _streamInterceptors.Add(interceptor); + + // 注册到各个拦截器 + InputStreamInterceptor.RegisterInterceptor(interceptor); + OutputStreamInterceptor.RegisterInterceptor(interceptor); + LogStreamInterceptor.RegisterInterceptor(interceptor); + + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[PluginManager] 已注册流拦截器: {interceptor.GetType().Name}"); + } + } + + // 新增插件事件通知方法 + internal static void NotifyPluginsOnOutput(string outputPath, string outputType) + { + foreach (var plugin in _plugins) + { + try + { + plugin.OnOutputGenerated(outputPath, outputType); + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] Output notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } + } + + internal static void RedirectOutput(string originalPath, string newPath) + { + foreach (var plugin in _plugins) + { + try + { + plugin.OnOutputGenerated(newPath, "redirected"); + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] Output redirection failed for {plugin.GetType().Name}: {ex.Message}"); + } + } + } + + internal static void NotifyPluginsOnLog(string logMessage, PluginLogLevel logLevel) + { + foreach (var plugin in _plugins) + { + try + { + plugin.OnLogGenerated(logMessage, logLevel); + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] Log notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } + } + + // 新增插件输入事件通知方法 + internal static void NotifyPluginsOnInput(object args, object option) + { + foreach (var plugin in _plugins) + { + try + { + plugin.OnInputReceived(args, option); + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Input notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } + } + + public static void LoadPlugins() + { + try + { + // 加载配置 + LoadConfig(); + + // 查找并加载所有实现了IPlugin接口的类 + var pluginTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.Namespace == "N_m3u8DL_RE.Plugin" && + t.Name.EndsWith("Plugin") && + !t.IsInterface && + !t.IsAbstract); + + Console.WriteLine($"[Plugin] Found {pluginTypes.Count()} plugin types"); + + foreach (var type in pluginTypes) + { + try + { + Console.WriteLine($"[Plugin] Creating instance of: {type.FullName}"); + if (Activator.CreateInstance(type) is IPlugin plugin) + { + // 检查配置中是否启用了该插件 + var pluginName = type.Name.Replace("Plugin", ""); + var isEnabled = IsPluginEnabled(pluginName); + + Console.WriteLine($"[Plugin] Plugin {pluginName} enabled: {isEnabled}"); + + if (isEnabled) + { + plugin.Initialize(_config); + _plugins.Add(plugin); + + // 检查是否实现流拦截器接口 + if (plugin is IStreamInterceptor interceptor) + { + RegisterStreamInterceptor(interceptor); + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[PluginManager] Registered stream interceptor: {pluginName}"); + } + + Console.WriteLine($"[Plugin] Loaded plugin: {pluginName}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Failed to create instance of {type.Name}: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"[Plugin] Inner exception: {ex.InnerException.Message}"); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] LoadPlugins failed: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"[Plugin] Inner exception: {ex.InnerException.Message}"); + } + } + } + + private static void LoadConfig() + { + var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "extend", "PluginConfig.json"); + if (File.Exists(configPath)) + { + try + { + var json = File.ReadAllText(configPath); + + // 手动解析JSON,避免反射序列化限制 + _config = new PluginConfig(); + + if (json.Contains("\"BatchDownload\"")) + { + var batchEnabled = ExtractJsonBoolValue(json, "BatchDownload", "Enabled"); + var batchFile = ExtractJsonValue(json, "BatchFile", "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt"); + var createSubdirs = ExtractJsonBoolValue(json, "BatchDownload", "CreateSubdirectories"); + + // 不再创建BatchDownloadConfig对象,插件将直接读取配置 + Console.WriteLine($"[Plugin] BatchDownload config: Enabled={batchEnabled}, File={batchFile}, CreateSubdirectories={createSubdirs}"); + } + + if (json.Contains("\"UASwitcher\"")) + { + var uaEnabled = ExtractJsonBoolValue(json, "UASwitcher", "Enabled"); + _config.UASwitcher = new UASwitcherConfig + { + Enabled = uaEnabled + }; + Console.WriteLine($"[Plugin] UASwitcher config: Enabled={uaEnabled}"); + } + + if (json.Contains("\"ProxySwitcher\"")) + { + var proxyEnabled = ExtractJsonBoolValue(json, "ProxySwitcher", "Enabled"); + _config.ProxySwitcher = new ProxySwitcherConfig + { + Enabled = proxyEnabled + }; + Console.WriteLine($"[Plugin] ProxySwitcher config: Enabled={proxyEnabled}"); + } + + if (json.Contains("\"StreamInterceptor\"")) + { + var streamInterceptorEnabled = ExtractJsonBoolValue(json, "StreamInterceptor", "Enabled"); + var interceptLevel = ExtractJsonValue(json, "InterceptLevel", "Info"); + var logDestination = ExtractJsonValue(json, "LogDestination", "Console"); + + _config.StreamInterceptor = new StreamInterceptorConfig + { + Enabled = streamInterceptorEnabled, + InterceptLevel = interceptLevel, + LogDestination = logDestination + }; + Console.WriteLine($"[Plugin] StreamInterceptor config: Enabled={streamInterceptorEnabled}, InterceptLevel={interceptLevel}, LogDestination={logDestination}"); + } + + Console.WriteLine($"[Plugin] Config loaded manually"); + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Manual config loading failed: {ex.Message}"); + _config = new PluginConfig(); + } + } + else + { + _config = new PluginConfig(); + } + } + + private static string ExtractJsonValue(string json, string propertyName, string defaultValue) + { + var pattern = $"\\\"{propertyName}\\\":\\s*\\\"([^\\\"]*)\\\""; + var match = System.Text.RegularExpressions.Regex.Match(json, pattern); + return match.Success ? match.Groups[1].Value : defaultValue; + } + + private static bool ExtractJsonBoolValue(string json, string sectionName, string propertyName) + { + // 尝试多种模式,因为JSON格式可能有变化 + var patterns = new[] + { + $"\\\"{sectionName}\\\":\\s*\\{{\"\\\"{propertyName}\\\":\\s*(true|false)", + $"\\\"{sectionName}\\\":\\s*\\{{\"\\\"{propertyName}\\\":\\s*(true|false)", + $"\\\"{sectionName}\\\":\\s*\\{{[^}}]*\\\"{propertyName}\\\":\\s*(true|false)" + }; + + foreach (var pattern in patterns) + { + var match = System.Text.RegularExpressions.Regex.Match(json, pattern); + if (match.Success) + { + Console.WriteLine($"[Plugin] Found {sectionName}.{propertyName} with pattern: {pattern}"); + Console.WriteLine($"[Plugin] Value: {match.Groups[1].Value}"); + return match.Groups[1].Value == "true"; + } + } + + Console.WriteLine($"[Plugin] Could not find {sectionName}.{propertyName}"); + return false; + } + + private static bool IsPluginEnabled(string pluginName) + { + return pluginName switch + { + "UASwitcher" => _config?.UASwitcher?.Enabled ?? false, + "ProxySwitcher" => _config?.ProxySwitcher?.Enabled ?? false, + "BatchDownload" => ExtractBatchDownloadEnabledFromConfig(), + "StreamInterceptor" => _config?.StreamInterceptor?.Enabled ?? false, + _ => false + }; + } + + private static bool ExtractBatchDownloadEnabledFromConfig() + { + try + { + var configPath = "extend/PluginConfig.json"; + if (File.Exists(configPath)) + { + var json = File.ReadAllText(configPath); + + // 查找"BatchDownload"部分 + var batchStart = json.IndexOf("\"BatchDownload\""); + if (batchStart == -1) return false; + + // 查找BatchDownload部分的结束位置 + var batchEnd = json.IndexOf("}", batchStart); + if (batchEnd == -1) return false; + + // 在BatchDownload部分内查找"Enabled"字段 + var enabledStart = json.IndexOf("\"Enabled\"", batchStart, batchEnd - batchStart); + if (enabledStart == -1) return false; + + var valueStart = json.IndexOf(":", enabledStart) + 1; + var valueEnd = json.IndexOf(",", valueStart); + if (valueEnd == -1 || valueEnd > batchEnd) valueEnd = json.IndexOf("}", valueStart); + + if (valueStart > 0 && valueEnd > valueStart) + { + var enabledValue = json.Substring(valueStart, valueEnd - valueStart).Trim(); + return enabledValue.Equals("true", StringComparison.OrdinalIgnoreCase); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] Failed to extract BatchDownload Enabled from config: {ex.Message}"); + } + + return false; // 默认值 + } + + public static List GetPlugins() => _plugins; + + public static void OnFileDownloaded(string filePath) + { + _downloadCount++; + + foreach (var plugin in _plugins) + { + plugin.OnFileDownloaded(filePath, _downloadCount); + } + } + + public static int GetDownloadCount() => _downloadCount; + + public static PluginConfig? GetConfig() => _config; + } + + public interface IPlugin + { + void Initialize(PluginConfig? config); + void OnFileDownloaded(string filePath, int downloadCount); + + // 新增插件接口方法 - 使用更通用的参数类型 + void OnInputReceived(object args, object option); + void OnOutputGenerated(string outputPath, string outputType); + void OnLogGenerated(string logMessage, PluginLogLevel logLevel); + } + + public interface IStreamInterceptor + { + // 输入流拦截 + string[] InterceptInput(string[] originalArgs); + object InterceptOptions(object originalOption); + + // 输出流拦截 + string InterceptOutput(string originalOutput, string outputType); + void OnOutputRedirect(string originalPath, string newPath); + + // 日志流拦截 + string InterceptLog(string originalLog, PluginLogLevel level); + void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination); + } + + public class PluginConfig + { + public UASwitcherConfig? UASwitcher { get; set; } + public ProxySwitcherConfig? ProxySwitcher { get; set; } + public StreamInterceptorConfig? StreamInterceptor { get; set; } + // BatchDownload配置现在由插件直接读取,不再通过PluginConfig传递 + } + + public class UASwitcherConfig + { + public bool Enabled { get; set; } = false; + public List UserAgents { get; set; } = new List(); + } + + public class ProxySwitcherConfig + { + public bool Enabled { get; set; } = false; + public string ClashApiUrl { get; set; } = "http://127.0.0.1:9090"; + public int SwitchInterval { get; set; } = 3; + } + + public class StreamInterceptorConfig + { + public bool Enabled { get; set; } = false; + public string InterceptLevel { get; set; } = "Info"; + public string LogDestination { get; set; } = "Console"; + } +} \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/BatchDownload\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/src/N_m3u8DL-RE/extend/extend-document/BatchDownload\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 00000000..7d5f5a88 --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/BatchDownload\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,336 @@ +# BatchDownload 插件开发文档 + +## 概述 + +BatchDownload 插件为 N_m3u8DL-RE 工具添加了批量下载功能,支持从配置文件或默认输入文件中读取多个URL进行批量处理。该插件确保每个URL生成包含原始URL信息的唯一文件名。 + +## 开发过程中文件修改详细记录 + +### 1. 配置架构优化 (最新修改) + +#### 配置统一化 +- **删除文件**: `BatchDownloadConfig.cs` - 移除了冗余的配置类 +- **统一配置源**: 所有配置现在直接从 `PluginConfig.json` 读取 +- **输入文件统一**: 使用统一的输入文件路径 `extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt` + +#### 配置解析优化 +**在 BatchDownloadPlugin-and-input-output/BatchDownloadPlugin.cs 中新增直接JSON解析方法**: +```csharp +private bool ExtractEnabledFromConfig() +private bool ExtractCreateSubdirectoriesFromConfig() +``` +- **功能**: 直接从 PluginConfig.json 中提取配置值 +- **优势**: 消除配置冲突,减少硬编码依赖 + +**在 PluginManager.cs 中新增配置提取方法**: +```csharp +private static bool ExtractBatchDownloadEnabledFromConfig() +``` +- **功能**: 为 Program.cs 提供统一的配置获取接口 + +#### 插件实例获取优化 +**在 Program.cs 中优化插件检测逻辑**: +- **移除**: 旧的反射配置获取方式 (`config?.BatchDownload`) +- **新增**: 使用 `ExtractBatchDownloadEnabledFromConfig()` 方法 +- **改进**: 插件实例获取逻辑更加稳定可靠 + +### 2. 在 `Program.cs` 中的修改 + +#### 新增方法 + +**在 Program.cs:800 行附近** 新增了 `GetUniqueFileNameFromUrl` 方法 +```csharp +static string GetUniqueFileNameFromUrl(string url, int batchIndex, int totalBatches) +``` +- **功能**: 根据URL和批量索引生成唯一文件名 +- **参数**: + - `url`: 源URL地址 + - `batchIndex`: 当前批量索引 (1-based) + - `totalBatches`: 总批量数 +- **返回值**: 包含URL信息和时间戳的唯一文件名 +- **文件名格式**: `{URL基础名}_batch{索引}_of_{总数}_{时间戳}` + +**在 Program.cs:561 行附近** 新增了 `ExecuteBatchDownload` 方法 +```csharp +static async Task ExecuteBatchDownload(dynamic batchPlugin, MyOption option) +``` +- **功能**: 执行批量下载逻辑 +- **参数**: + - `batchPlugin`: 批量下载插件实例 + - `option`: 下载选项配置 +- **流程**: 读取URL列表 → 循环处理每个URL → 调用ExecuteSingleDownload + +#### 修改函数 + +**修改了 ExecuteSingleDownload 方法** (Program.cs:675行附近) +- **新增参数**: + - `int batchIndex = 0`: 批量索引 + - `int totalBatches = 0`: 总批量数 + - `bool batchDownload = false`: 是否为批量下载模式 +- **修改内容**: 在批量模式下自动生成唯一文件名,包含URL信息和批量索引 + +**修改了批量下载循环逻辑** (Program.cs:624-626行) +- **新增**: 在每次循环开始时重置 `option.SaveName = null` +- **目的**: 确保每个URL都能生成唯一的文件名 + +#### 新增配置支持 + +**在 Program.cs:261-344行** 添加了批量下载插件检测逻辑 (已优化) +- 检测插件配置中的 `BatchDownload.Enabled` 属性 +- 通过反射调用插件方法获取URL列表和配置 +- 支持从插件或直接读取配置文件获取URL + +### 2. 在 `CommandLine/CommandInvoker.cs` 中的修改 + +#### 新增参数 + +**在 CommandInvoker.cs:34行** 新增了 `--batch` 命令行选项 +```csharp +private static readonly Option BatchMode = new("--batch") { Description = "Enable batch download mode" }; +``` + +**在 CommandInvoker.cs:36行** 修改了保存目录选项 +```csharp +private static readonly Option SaveDir = new("--save-dir") { Description = ResString.cmd_saveDir }; +``` + +#### 修改函数 + +**修改了 GetOptions 方法** (CommandInvoker.cs:617, 632行) +- **新增**: `BatchMode = result.GetValue(BatchMode)` - 获取批量模式标志 +- **新增**: `SaveDir = result.GetValue(SaveDir)` - 获取保存目录 + +**修改了 RootCommand 配置** (CommandInvoker.cs:732行) +- **新增**: 将 `BatchMode` 和 `SaveDir` 添加到根命令选项中 + +### 3. 在 `CommandLine/MyOption.cs` 中的修改 + +#### 新增属性 + +**在 MyOption.cs:18行** 新增了批量模式属性 +```csharp +public bool BatchMode { get; set; } +``` + +### 4. 在 `N_m3u8DL-RE.csproj` 中的修改 + +**无修改** - 批量下载功能作为插件扩展添加,无需修改主项目配置。 + +### 5. 在 `Downloader/SimpleDownloader.cs` 中的修改 + +**无修改** - 批量下载功能主要涉及程序逻辑层,不需要修改下载器组件。 + +## BatchDownloadPlugin 调用链和参数传递链 + +### 调用链概览 + +``` +主程序启动 → 检测批量下载配置 → 识别批量下载模式 → +ExecuteBatchDownload → 循环处理URL列表 → ExecuteSingleDownload → +生成唯一文件名 → 执行下载 → 写出文件 +``` + +### 详细调用链 + +1. **程序启动** (`Program.cs:Main`) + - 解析命令行参数,包括 `--batch` 和 `--save-dir` + - 检测是否启用批量下载模式 + +2. **插件检测** (`Program.cs:261-344`) + ```csharp + // 通过反射获取插件配置 + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + var getConfigMethod = pluginManagerType.GetMethod("GetConfig"); + var config = getConfigMethod.Invoke(null, null); + ``` + +3. **批量下载启动** (`Program.cs:323-325`) + ```csharp + await ExecuteBatchDownload(batchPlugin, option); + ``` + +4. **URL列表获取** (`Program.cs:568-579`) + - 优先从插件获取URL列表:`batchPlugin.GetUrlList()` + - 回退到直接读取配置文件:`/workspace/input.txt` + +5. **批量循环处理** (`Program.cs:624-626`) + ```csharp + // 重置文件名确保唯一性 + option.SaveName = null; + await ExecuteSingleDownload(url, option, i + 1, urls.Count, batchDownload: true); + ``` + +6. **单URL下载** (`Program.cs:675-725`) + - 检测批量模式,调用 `GetUniqueFileNameFromUrl()` + - 生成唯一的文件名包含URL信息 + - 执行下载和文件写出 + +### 参数传递链 + +``` +命令行参数 → MyOption对象 → ExecuteBatchDownload → ExecuteSingleDownload → +GetUniqueFileNameFromUrl → 生成文件名 → 文件写出 +``` + +**参数传递细节**: +- `MyOption.BatchMode`: 控制是否启用批量模式 +- `MyOption.SaveDir`: 批量下载的输出目录 +- `batchIndex`: 批量中的当前索引 (1-based) +- `totalBatches`: 总批量数 +- `url`: 当前处理的URL地址 +- `option.SaveName`: 文件名,批量模式下自动生成 + +## BatchDownloadPlugin 使用方法 + +### 命令行使用 + +#### 基本语法 +```bash +dotnet run -- --batch [选项] +``` + +#### 常用参数 +```bash +# 启用批量下载模式 +--batch + +# 指定输出目录 +--save-dir /path/to/output + +# 其他可用参数(与单URL下载相同) +--threads 4 +--log-level info +--write-meta-json +``` + +#### 使用示例 + +**示例1**: 使用默认配置 +```bash +dotnet run -- --batch --save-dir /workspace/mpegts.js/demo/output +``` +- 从 `/workspace/input.txt` 读取URL列表 +- 输出到指定目录 +- 自动生成包含URL信息的唯一文件名 + +**示例2**: 带日志输出 +```bash +dotnet run -- --batch --save-dir /output --log-level debug +``` + +### 输入文件格式 + +批量下载支持从配置文件读取URL,默认文件为 `/workspace/input.txt`。 + +**文件格式**: +``` +# 注释行以#开头 +https://example1.com/video1.m3u8 +https://example2.com/video2.m3u8 +https://example3.com/video3.m3u8 +``` + +**文件要求**: +- 每行一个URL +- 支持 `#` 开头的注释行 +- 空行会被忽略 +- 支持 `.m3u8` 和 `.mpd` 格式 + +### 文件命名规则 + +批量下载自动生成包含原始URL信息的文件名: + +**命名格式**: +``` +{URL基础名}_batch{索引}_of_{总数}_{时间戳}.{扩展名} +``` + +**示例**: +``` +7d7157190fe28708-9c54c7045ab91221e04441539478c65f-hls_720p_2_batch01_of_02_2025-12-15_03-27-14.mp4 +68cb0f69105349cd-1d864ca604cae351faf616aca3a356ba-hls_720p_2_batch02_of_02_2025-12-15_03-27-15.mp4 +``` + +**命名特点**: +- 包含URL来源的哈希值,确保文件名唯一 +- 显示批量进度信息 (`batch01_of_02`) +- 精确到秒的时间戳,避免冲突 +- 保留原始文件的扩展名 + +### 配置选项 + +批量下载支持通过 `PluginConfig.json` 进行配置: + +```json +{ + "BatchDownload": { + "Enabled": true, + "CreateSubdirectories": false, + "MaxConcurrency": 3 + } +} +``` + +**配置项说明**: +- `Enabled`: 是否启用批量下载功能 +- `CreateSubdirectories`: 是否为每个URL创建子目录 +- `MaxConcurrency`: 最大并发下载数(预留) + +### 错误处理 + +批量下载包含完善的错误处理机制: + +1. **单URL失败不影响整体**: 每个URL独立处理,一个失败不影响其他URL +2. **详细日志记录**: 记录成功和失败的URL数量 +3. **优雅降级**: 插件不可用时自动回退到直接读取配置文件 +4. **文件名冲突避免**: 通过多种机制确保文件名唯一 + +### 日志输出 + +批量下载提供详细的日志信息: + +``` +[BatchDownload] Detecting batch mode... BatchMode=True, PluginEnabled=True, PluginInstance=BatchDownloadPlugin +[BatchDownload] Starting batch download with 2 URLs +[BatchDownload] Processing URL 1/2: https://example1.com/video.m3u8 +[BatchDownload] Processing URL 2/2: https://example2.com/video.m3u8 +[BatchDownload] Batch download completed. Success: 2, Failed: 0 +``` + +## 技术特性 + +### 关键创新 + +1. **文件名唯一性保证**: + - URL哈希值 + 批量索引 + 时间戳 + - 多重防冲突机制 + +2. **插件架构集成**: + - 通过反射机制动态检测插件 + - 优雅降级到配置文件模式 + +3. **最小侵入性修改**: + - 保持原有API接口不变 + - 添加可选参数支持向后兼容 + +4. **配置灵活性**: + - 支持命令行参数 + - 支持配置文件设置 + - 支持插件动态配置 + +### 兼容性保证 + +- **向后兼容**: 原有单URL下载功能完全保持不变 +- **API稳定**: 不修改现有方法的签名,通过可选参数实现 +- **配置兼容**: 支持原有的所有配置选项 + +## 总结 + +BatchDownload 插件成功为 N_m3u8DL-RE 添加了强大的批量下载功能,通过最小侵入性的修改实现了以下目标: + +1. **功能完整性**: 支持批量URL处理,每个URL生成唯一文件名 +2. **用户体验**: 简单的命令行接口,详细的进度反馈 +3. **技术健壮性**: 完善的错误处理和日志记录 +4. **可维护性**: 清晰的代码结构,完善的文档记录 + +该插件已经过充分测试,能够正确处理多个URL并生成包含原始URL信息的唯一文件名,满足了批量下载的所有需求。 \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/Inputstreaminterceptor.cs\350\276\223\345\205\245\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/src/N_m3u8DL-RE/extend/extend-document/Inputstreaminterceptor.cs\350\276\223\345\205\245\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 00000000..1aad7a90 --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/Inputstreaminterceptor.cs\350\276\223\345\205\245\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,526 @@ +# InputStreamInterceptor.cs输入流接管插件开发文档 + +## 概述 + +InputStreamInterceptor是N_m3u8DL-RE插件系统中的输入流接管组件,负责拦截和处理命令行输入流,包括命令行参数和选项对象。本文档详细介绍了InputStreamInterceptor的架构设计、核心功能、使用方式以及与PluginManager的集成关系。 + +## 核心架构设计 + +### 1. 整体架构 + +InputStreamInterceptor采用静态类设计,作为输入流接管的实现层,与PluginManager形成统一的拦截管理体系: + +```csharp +/// +/// 输入流拦截器类 +/// +/// 【接管说明】此类的功能由 PluginManager.cs 接管和调用: +/// - 在 CommandInvoker.cs 的 InvokeArgs 方法开始处被调用 +/// - 插件系统通过 PluginManager.cs 统一管理和调用此拦截器 +/// - 插件的输入拦截功能由 PluginManager.NotifyPluginsOnInput() 方法协调 +/// +/// 设计目的: +/// 1. 提供参数和选项的拦截机制 +/// 2. 支持插件对输入流进行处理和修改 +/// 3. 为批量下载等高级插件功能提供输入流控制 +/// +public class InputStreamInterceptor +{ + // 静态拦截器列表 - 用于存储所有注册的拦截器 + private static List _interceptors = new List(); +} +``` + +### 2. 架构特点 + +- **静态单例模式**:所有拦截器共享同一个实例,保证拦截链的一致性 +- **链式拦截机制**:支持多个拦截器的依次调用,形成拦截链 +- **PluginManager托管**:由PluginManager统一管理拦截器的注册和调用 +- **非侵入性设计**:不修改主程序逻辑,通过反射机制集成 + +## 核心功能模块 + +### 1. 拦截器注册机制 + +#### 注册方法实现 + +```csharp +/// +/// 注册拦截器 +/// 【接管说明】此方法由插件系统调用,插件管理器通过PluginManager.cs统一管理 +/// +/// +/// 要注册的拦截器实例 +public static void RegisterInterceptor(IStreamInterceptor interceptor) +{ + _interceptors.Add(interceptor); +} +``` + +#### 注册流程 + +1. **插件初始化**:插件实现IStreamInterceptor接口 +2. **统一注册**:PluginManager.RegisterStreamInterceptor()方法统一注册 +3. **三个拦截器注册**:InputStreamInterceptor.RegisterInterceptor()、OutputStreamInterceptor.RegisterInterceptor()、LogStreamInterceptor.RegisterInterceptor() +4. **拦截链构建**:所有拦截器按注册顺序形成拦截链 + +### 2. 参数拦截机制 + +#### 参数拦截实现 + +```csharp +/// +/// 拦截命令行参数 +/// 【接管说明】此方法由 PluginManager.cs 在 CommandInvoker.cs 中统一调用 +/// +/// 工作流程: +/// 1. PluginManager.NotifyPluginsOnInput() 触发输入事件 +/// 2. 各插件的 OnInputReceived() 方法会被调用 +/// 3. 插件可以通过 IStreamInterceptor 接口处理参数 +/// +/// 原始命令行参数数组 +/// 经过拦截器处理后的参数数组 +public static string[] InterceptArgs(string[] originalArgs) +{ + var result = originalArgs; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptInput(result); + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; +} +``` + +#### 拦截流程 + +1. **输入接收**:接收原始命令行参数数组 +2. **链式处理**:依次调用所有注册拦截器的InterceptInput方法 +3. **参数修改**:拦截器可以修改、添加、删除参数 +4. **结果返回**:返回经过所有拦截器处理的参数数组 +5. **错误处理**:单个拦截器错误不影响整体流程 + +### 3. 选项对象拦截机制 + +#### 选项拦截实现 + +```csharp +/// +/// 拦截选项对象 +/// 【接管说明】此方法由 PluginManager.cs 在 CommandInvoker.cs 中统一调用 +/// +/// 工作流程: +/// 1. PluginManager.NotifyPluginsOnInput() 触发输入事件 +/// 2. 各插件的 OnInputReceived() 方法会被调用 +/// 3. 插件可以通过 IStreamInterceptor 接口处理选项对象 +/// +/// 原始选项对象 +/// 经过拦截器处理后的选项对象 +public static object InterceptOptions(object originalOption) +{ + var result = originalOption; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptOptions(result); + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; +} +``` + +#### 选项对象处理 + +1. **对象接收**:接收命令行解析生成的选项对象 +2. **类型无关处理**:使用object类型支持任意选项对象 +3. **链式修改**:依次调用拦截器的InterceptOptions方法 +4. **灵活扩展**:支持复杂选项对象的动态修改 + +## 与PluginManager的集成关系 + +### 1. 统一管理架构 + +InputStreamInterceptor作为输入流接管的实现层,由PluginManager统一管理: + +```csharp +public static void RegisterStreamInterceptor(IStreamInterceptor interceptor) +{ + if (!_streamInterceptors.Contains(interceptor)) + { + _streamInterceptors.Add(interceptor); + + // 统一注册到各个拦截器 + InputStreamInterceptor.RegisterInterceptor(interceptor); + OutputStreamInterceptor.RegisterInterceptor(interceptor); + LogStreamInterceptor.RegisterInterceptor(interceptor); + } +} +``` + +### 2. 事件通知机制 + +```csharp +/// +/// 通知插件处理输入事件 +/// +internal static void NotifyPluginsOnInput(object args, object option) +{ + foreach (var plugin in _plugins) + { + try + { + plugin.OnInputReceived(args, option); + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Input notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } +} +``` + +## 与主程序的集成 + +### 1. CommandInvoker集成点 + +在CommandInvoker.cs的InvokeArgs方法中集成输入拦截: + +```csharp +public void InvokeArgs(string[] args) +{ + try + { + // 输入流拦截 + args = InputStreamInterceptor.InterceptArgs(args); + + // 插件输入事件通知 + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var notifyInputMethod = pluginManagerType.GetMethod("NotifyPluginsOnInput", + BindingFlags.NonPublic | BindingFlags.Static); + if (notifyInputMethod != null) + { + // 解析参数用于通知 + var rootCommand = new RootCommand(VERSION_INFO); + var parser = new CommandLineBuilder(rootCommand) + .UseDefaults() + .Build(); + + var parseResult = parser.Parse(args); + if (parseResult.GetValueForOption(Input) is MyOption option) + { + notifyInputMethod.Invoke(null, new object[] { args, option }); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Failed to process input: {ex.Message}"); + } + + // 继续原有逻辑... +} +``` + +### 2. 集成流程 + +1. **参数接收**:CommandInvoker接收原始命令行参数 +2. **输入拦截**:调用InputStreamInterceptor.InterceptArgs进行参数拦截 +3. **事件通知**:调用PluginManager.NotifyPluginsOnInput通知所有插件 +4. **插件处理**:各插件的OnInputReceived方法被调用 +5. **后续处理**:继续原有的参数解析和命令执行逻辑 + +## 接口定义和实现 + +### 1. IStreamInterceptor接口定义 + +```csharp +public interface IStreamInterceptor +{ + // 输入流拦截 + string[] InterceptInput(string[] originalArgs); + object InterceptOptions(object originalOption); + + // 输出流拦截 + string InterceptOutput(string originalOutput, string outputType); + void OnOutputRedirect(string originalPath, string newPath); + + // 日志流拦截 + string InterceptLog(string originalLog, PluginLogLevel level); + void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination); +} +``` + +### 2. 输入拦截接口实现 + +```csharp +public class StreamInterceptorPlugin : IPlugin, IStreamInterceptor +{ + public string[] InterceptInput(string[] originalArgs) + { + // 参数拦截逻辑 + var modifiedArgs = new List(originalArgs); + + // 示例:添加批处理模式参数 + if (!modifiedArgs.Contains("--batch")) + { + modifiedArgs.Add("--batch"); + } + + return modifiedArgs.ToArray(); + } + + public object InterceptOptions(object originalOption) + { + // 选项对象拦截逻辑 + // 可以修改选项对象的属性 + + return originalOption; + } +} +``` + +## 使用场景和示例 + +### 1. 批处理插件示例 + +```csharp +public class BatchDownloadPlugin : IPlugin, IStreamInterceptor +{ + public void Initialize(PluginConfig? config) + { + // 插件初始化 + } + + public string[] InterceptInput(string[] originalArgs) + { + // 检查是否包含批量URL文件参数 + var argsList = new List(originalArgs); + + if (!argsList.Contains("--batch") && !argsList.Contains("--batch-urls")) + { + // 自动添加批处理模式 + argsList.Add("--batch"); + } + + return argsList.ToArray(); + } + + public object InterceptOptions(object originalOption) + { + // 修改选项对象 + var option = originalOption as MyOption; + if (option != null && !option.BatchMode) + { + // 启用批处理模式 + // option.BatchMode = true; + } + + return originalOption; + } +} +``` + +### 2. 代理切换插件示例 + +```csharp +public class ProxySwitcherPlugin : IPlugin, IStreamInterceptor +{ + public string[] InterceptInput(string[] originalArgs) + { + // 检查是否需要添加代理参数 + var argsList = new List(originalArgs); + + if (ShouldUseProxy()) + { + // 自动添加代理设置 + if (!argsList.Contains("--proxy")) + { + argsList.Add("--proxy"); + argsList.Add(GetCurrentProxy()); + } + } + + return argsList.ToArray(); + } +} +``` + +## 错误处理机制 + +### 1. 异常捕获策略 + +```csharp +public static string[] InterceptArgs(string[] originalArgs) +{ + var result = originalArgs; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptInput(result); + } + catch (Exception ex) + { + // 记录错误但不中断拦截流程 + Console.WriteLine($"[InputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; +} +``` + +### 2. 错误处理原则 + +- **隔离错误**:单个拦截器错误不影响其他拦截器 +- **错误记录**:详细记录错误信息便于调试 +- **继续处理**:错误后继续执行后续拦截器 +- **原始参数保留**:出现错误时保留原始参数 + +### 3. 常见错误类型 + +1. **参数格式错误**:拦截器修改参数格式错误 +2. **选项对象类型错误**:选项对象类型转换失败 +3. **空指针异常**:拦截器访问空对象 +4. **权限错误**:访问受限资源或方法 + +## 性能优化 + +### 1. 拦截器优化 + +- **早期退出**:如果拦截器列表为空,直接返回原始参数 +- **高效链表**:使用List存储拦截器,支持快速添加和遍历 +- **异常隔离**:使用try-catch隔离异常,避免性能损耗 + +### 2. 内存管理 + +- **对象复用**:避免在拦截器中创建大量临时对象 +- **及时释放**:拦截器处理完成后及时释放临时资源 +- **字符串优化**:合理使用字符串操作,避免过多字符串拼接 + +### 3. 并发安全 + +- **线程安全**:静态成员访问需要考虑线程安全问题 +- **同步机制**:在多线程环境中需要适当的同步机制 +- **状态管理**:避免拦截器之间共享可变状态 + +## 调试和监控 + +### 1. 调试输出 + +```csharp +public static void RegisterInterceptor(IStreamInterceptor interceptor) +{ + if (!_interceptors.Contains(interceptor)) + { + _interceptors.Add(interceptor); + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[InputInterceptor] 已注册拦截器: {interceptor.GetType().Name}"); + } +} +``` + +### 2. 调试技巧 + +- **Debug.WriteLine**:使用Debug输出避免触发Console拦截 +- **日志记录**:记录拦截器注册和调用信息 +- **参数跟踪**:跟踪参数修改过程 + +### 3. 监控指标 + +- **拦截器数量**:监控注册的拦截器数量 +- **执行时间**:监控拦截处理耗时 +- **错误率**:监控拦截器执行错误率 + +## 最佳实践 + +### 1. 拦截器开发 + +- **接口实现**:确保正确实现IStreamInterceptor接口 +- **参数验证**:验证输入参数的合法性和完整性 +- **异常处理**:正确处理可能出现的异常情况 +- **性能考虑**:避免在拦截器中进行耗时操作 + +### 2. 集成使用 + +- **PluginManager管理**:通过PluginManager统一管理拦截器 +- **注册顺序**:考虑拦截器的注册顺序对结果的影响 +- **状态管理**:避免拦截器之间的状态冲突 + +### 3. 测试验证 + +- **单元测试**:对每个拦截器进行单元测试 +- **集成测试**:测试整个拦截链的功能 +- **性能测试**:验证拦截器对性能的影响 + +## 故障排除 + +### 1. 常见问题 + +**拦截器不生效** +- 检查拦截器是否正确注册到PluginManager +- 检查拦截器是否正确实现IStreamInterceptor接口 +- 检查调用时机是否正确 + +**参数修改失效** +- 检查拦截器修改逻辑是否正确 +- 检查是否有其他拦截器覆盖了修改 +- 检查参数类型和格式是否匹配 + +**选项对象修改无效** +- 检查选项对象类型转换是否正确 +- 检查属性设置是否有效 +- 检查对象引用是否正确 + +### 2. 调试步骤 + +1. **检查注册**:确认拦截器已正确注册到InputStreamInterceptor +2. **检查调用**:确认InputStreamInterceptor.InterceptArgs被正确调用 +3. **检查实现**:检查拦截器的InterceptInput方法实现 +4. **检查异常**:查看是否有异常被捕获但未处理 + +### 3. 修复建议 + +- **重新注册**:删除并重新注册拦截器 +- **清理缓存**:清理可能的缓存或状态 +- **重新编译**:重新编译项目确保最新代码生效 + +## 扩展指南 + +### 1. 新增拦截功能 + +1. **实现接口**:在拦截器类中实现IStreamInterceptor接口 +2. **添加逻辑**:在InterceptInput方法中添加具体拦截逻辑 +3. **注册拦截器**:通过PluginManager.RegisterStreamInterceptor注册 +4. **测试验证**:测试新功能是否正常工作 + +### 2. 复杂拦截场景 + +**多参数依赖处理** +- 拦截器之间可能存在参数依赖关系 +- 需要考虑拦截器的执行顺序 +- 可以通过配置文件控制拦截器优先级 + +**动态参数生成** +- 根据环境或配置动态生成参数 +- 支持条件性参数添加 +- 提供参数模板机制 + +## 总结 + +InputStreamInterceptor作为N_m3u8DL-RE插件系统的输入流接管组件,提供了强大的命令行参数和选项对象拦截能力。通过与PluginManager的紧密集成,实现了统一、可扩展的输入流处理机制。 + +本文档详细介绍了InputStreamInterceptor的架构设计、功能实现、使用方式以及最佳实践,为开发者提供了完整的输入流拦截开发指南。通过合理使用InputStreamInterceptor,可以实现灵活的命令行参数处理和插件功能扩展。 \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/LogStreaminterceptor.cs\346\227\245\345\277\227\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/src/N_m3u8DL-RE/extend/extend-document/LogStreaminterceptor.cs\346\227\245\345\277\227\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 00000000..1f13e97a --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/LogStreaminterceptor.cs\346\227\245\345\277\227\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,872 @@ +# LogStreamInterceptor.cs日志流接管插件开发文档 + +## 概述 + +LogStreamInterceptor是N_m3u8DL-RE插件系统中的日志流接管组件,负责接管和重定向Console输出流(stdout和stderr),实现日志拦截、过滤和重定向功能。本文档详细介绍了LogStreamInterceptor的架构设计、核心功能、使用方式以及与PluginManager的集成关系。 + +## 核心架构设计 + +### 1. 整体架构 + +LogStreamInterceptor采用静态类设计,作为日志流接管的实现层,具有复杂的Console输出重定向机制和配置控制功能: + +```csharp +// 【日志拦截】由PluginManager.cs统一管理日志流拦截功能 +// 该类处理Console输出重定向和日志拦截逻辑,确保日志不丢失且可被插件拦截 +// 支持通过PluginConfig.json中的StreamInterceptor配置控制启用状态 + +public class LogStreamInterceptor +{ + private static List _interceptors = new List(); + private static StringWriter? _originalConsoleOut; + private static StringWriter? _originalConsoleError; + private static bool _isInitialized = false; + private static bool _isEnabled = false; // 新增:是否启用的标志 +} +``` + +### 2. 架构特点 + +- **Console输出重定向**:通过自定义StringWriter实现Console输出的重定向 +- **防递归设计**:使用Debug.WriteLine避免Console输出导致的递归调用 +- **初始化机制**:具有专门的初始化方法处理Console重定向 +- **链式拦截机制**:支持多个拦截器的依次调用 +- **PluginManager托管**:由PluginManager统一管理拦截器的注册和调用 +- **配置控制**:支持通过PluginConfig.json控制启用状态 +- **动态启用**:可根据配置动态启用或禁用拦截功能 + +### 3. 核心组件结构 + +```csharp +public class InterceptedStringWriter : StringWriter +{ + private readonly StringWriter _original; + private readonly string _streamType; + + public InterceptedStringWriter(StringWriter original, string streamType) + { + _original = original; + _streamType = streamType; + } + + public override void Write(string value) + { + var intercepted = LogStreamInterceptor.InterceptLog(value, PluginLogLevel.Info); + _original.Write(intercepted); + base.Write(intercepted); + } +} +``` + +## 核心功能模块 + +### 1. 初始化机制 + +#### 初始化实现 + +```csharp +/// +/// 初始化日志拦截器,重定向Console输出 +/// 【日志拦截】由PluginManager.cs统一调用此方法进行初始化 +/// +/// 是否启用日志拦截器,由配置决定 +public static void Initialize(bool enabled = false) +{ + _isEnabled = enabled; + + if (_isInitialized || !_isEnabled) return; + + try + { + // 【调试】记录Console.Out的类型 + var originalOutType = Console.Out.GetType().Name; + var originalErrorType = Console.Error.GetType().Name; + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] Console.Out类型: {originalOutType}, Console.Error类型: {originalErrorType}"); + + // 先输出初始化消息到实际控制台(避免被拦截) + Console.WriteLine("[LogInterceptor] 日志流拦截器初始化开始..."); + + // 保存原始Console引用 + _originalConsoleOut = new StringWriter(); + _originalConsoleError = new StringWriter(); + + // 复制当前的Console内容到StringWriter + var currentOut = Console.Out; + var currentError = Console.Error; + + // 创建拦截的StringWriter + var interceptedOut = new InterceptedStringWriter(_originalConsoleOut, "stdout"); + var interceptedErr = new InterceptedStringWriter(_originalConsoleError, "stderr"); + + // 输出初始化完成消息到实际控制台 + Console.WriteLine("[LogInterceptor] 日志流拦截器初始化完成"); + + _isInitialized = true; + + // 重定向Console输出 + Console.SetOut(interceptedOut); + Console.SetError(interceptedErr); + } + catch (Exception ex) + { + // 使用Debug输出错误信息,避免Console重定向问题 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 异常详情: {ex.StackTrace}"); + + try + { + // 尝试使用Console.WriteLine输出错误 + Console.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + } + catch + { + // 如果Console不可用,忽略 + } + } +} +``` + +#### 初始化流程 + +1. **启用检查**:检查是否启用,未启用则直接返回 +2. **重复检查**:检查是否已经初始化,避免重复初始化 +3. **状态设置**:设置启用状态标志 +4. **类型调试**:记录Console.Out和Console.Error的类型信息 +5. **消息输出**:先输出初始化消息到实际控制台(避免被拦截) +6. **引用保存**:保存原始Console引用以备恢复 +7. **拦截器创建**:创建自定义的InterceptedStringWriter +8. **输出重定向**:将Console输出重定向到拦截器 +9. **错误处理**:使用Debug输出错误信息避免递归 + +### 2. Console输出重定向机制 + +#### 自定义StringWriter实现 + +```csharp +public class InterceptedStringWriter : StringWriter +{ + private readonly StringWriter _original; + private readonly string _streamType; + + public InterceptedStringWriter(StringWriter original, string streamType) + { + _original = original; + _streamType = streamType; + } + + public override void Write(string value) + { + // 调用日志拦截器处理输出 + var intercepted = LogStreamInterceptor.InterceptLog(value, PluginLogLevel.Info); + + // 写入原始输出 + _original.Write(intercepted); + + // 写入拦截后的输出到基类 + base.Write(intercepted); + } + + public override void WriteLine(string value) + { + // 调用日志拦截器处理输出 + var intercepted = LogStreamInterceptor.InterceptLog(value, PluginLogLevel.Info); + + // 写入原始输出 + _original.WriteLine(intercepted); + + // 写入拦截后的输出到基类 + base.WriteLine(intercepted); + } +} +``` + +#### 重定向工作原理 + +1. **Console输出拦截**:所有Console.Write和Console.WriteLine调用被拦截 +2. **日志处理**:调用LogStreamInterceptor.InterceptLog方法处理日志 +3. **双重写入**:同时写入原始输出和拦截后的输出 +4. **链式处理**:依次调用所有注册拦截器的InterceptLog方法 + +### 3. 拦截器注册机制 + +#### 注册方法实现 + +```csharp +/// +/// 注册日志拦截器 +/// 【日志拦截】由PluginManager.cs统一管理拦截器注册 +/// +public static void RegisterInterceptor(IStreamInterceptor interceptor) +{ + if (!_isEnabled) + { + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 拦截器未启用,跳过注册: {interceptor.GetType().Name}"); + return; + } + + if (!_interceptors.Contains(interceptor)) + { + _interceptors.Add(interceptor); + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 已注册拦截器: {interceptor.GetType().Name}"); + } +} +``` + +#### 注册流程 + +1. **启用检查**:检查拦截器是否启用,未启用则跳过注册 +2. **重复检查**:检查拦截器是否已存在,避免重复注册 +3. **添加拦截器**:将新拦截器添加到拦截器列表 +4. **调试输出**:使用Debug.WriteLine记录注册信息 +5. **防递归设计**:避免Console输出导致的递归调用 + +### 4. 日志拦截机制 + +#### 日志拦截实现 + +```csharp +/// +/// 拦截并处理日志消息 +/// 【日志拦截】由PluginManager.cs统一调用此方法进行日志拦截处理 +/// +public static string InterceptLog(string originalLog, PluginLogLevel level) +{ + if (!_isEnabled) + return originalLog; + + if (string.IsNullOrEmpty(originalLog)) + return originalLog; + + var result = originalLog; + + // 依次调用所有拦截器进行处理 + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptLog(result, level); + if (string.IsNullOrEmpty(result)) + result = originalLog; // 如果被拦截为空,保留原始日志 + } + catch (Exception ex) + { + // 使用Debug输出错误信息,避免Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 拦截器 {interceptor.GetType().Name} 处理错误: {ex.Message}"); + } + } + + return result; +} +``` + +#### 拦截流程 + +1. **启用检查**:检查拦截器是否启用,未启用则直接返回原始日志 +2. **空值检查**:检查日志消息是否为空 +3. **链式处理**:依次调用所有注册拦截器的InterceptLog方法 +4. **结果保护**:如果拦截器返回空值,保留原始日志 +5. **错误隔离**:单个拦截器错误不影响其他拦截器 +6. **Debug输出**:使用Debug输出错误信息避免递归 + +### 5. 日志重定向机制 + +#### 重定向实现 + +```csharp +/// +/// 处理日志重定向事件 +/// 【日志拦截】由PluginManager.cs统一调用此方法处理日志重定向 +/// +public static void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination) +{ + if (!_isEnabled) + return; + + foreach (var interceptor in _interceptors) + { + try + { + interceptor.OnLogRedirect(originalLog, level, newDestination); + } + catch (Exception ex) + { + // 使用Debug输出错误信息,避免Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 拦截器 {interceptor.GetType().Name} 重定向错误: {ex.Message}"); + } + } +} +``` + +## 与PluginManager的集成关系 + +### 1. 统一管理架构 + +LogStreamInterceptor作为日志流接管的实现层,由PluginManager统一管理: + +```csharp +public static void RegisterStreamInterceptor(IStreamInterceptor interceptor) +{ + if (!_streamInterceptors.Contains(interceptor)) + { + _streamInterceptors.Add(interceptor); + + // 统一注册到各个拦截器 + InputStreamInterceptor.RegisterInterceptor(interceptor); + OutputStreamInterceptor.RegisterInterceptor(interceptor); + LogStreamInterceptor.RegisterInterceptor(interceptor); + } +} +``` + +### 2. 事件通知机制 + +```csharp +/// +/// 通知插件处理日志事件 +/// +internal static void NotifyPluginsOnLog(string logMessage, PluginLogLevel logLevel) +{ + foreach (var plugin in _plugins) + { + try + { + plugin.OnLogGenerated(logMessage, logLevel); + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] Log notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } +} +``` + +## 与主程序的集成 + +### 1. Program.cs集成点 + +在Program.cs的Main方法中初始化日志拦截器: + +```csharp +static async Task Main(string[] args) +{ + // 【日志拦截】由PluginManager.cs统一调用此方法进行日志流拦截初始化 + // 初始化日志拦截器,重定向Console输出到拦截器 + try + { + // 首先尝试通过Assembly.GetExecutingAssembly()获取当前程序集 + var executingAssembly = Assembly.GetExecutingAssembly(); + var logInterceptorType = executingAssembly.GetType("N_m3u8DL_RE.Plugin.LogStreamInterceptor"); + + if (logInterceptorType == null) + { + // 如果找不到,尝试通过Type.GetType + logInterceptorType = Type.GetType("N_m3u8DL_RE.Plugin.LogStreamInterceptor, N_m3u8DL-RE"); + } + + Console.WriteLine($"[LogInterceptor] 正在查找LogStreamInterceptor类型: {logInterceptorType != null}"); + + if (logInterceptorType != null) + { + // 检查配置是否启用StreamInterceptor + bool isLogInterceptorEnabled = false; + try + { + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var getConfigMethod = pluginManagerType.GetMethod("GetConfig"); + if (getConfigMethod != null) + { + var config = getConfigMethod.Invoke(null, null); + if (config != null) + { + var configType = config.GetType(); + var streamInterceptorProp = configType.GetProperty("StreamInterceptor"); + if (streamInterceptorProp != null) + { + var streamConfig = streamInterceptorProp.GetValue(config); + if (streamConfig != null) + { + var enabledProp = streamConfig.GetType().GetProperty("Enabled"); + if (enabledProp != null) + { + isLogInterceptorEnabled = (bool)(enabledProp.GetValue(streamConfig) ?? false); + } + } + } + } + } + } + } + catch (Exception configEx) + { + Console.WriteLine($"[LogInterceptor] 配置检查失败,使用默认设置: {configEx.Message}"); + } + + Console.WriteLine($"[LogInterceptor] StreamInterceptor配置启用状态: {isLogInterceptorEnabled}"); + + var initializeMethod = logInterceptorType.GetMethod("Initialize"); + if (initializeMethod != null) + { + Console.WriteLine("[LogInterceptor] 找到Initialize方法,正在调用..."); + try + { + // 根据配置决定是否启用日志拦截器 + initializeMethod.Invoke(null, new object[] { isLogInterceptorEnabled }); + Console.WriteLine($"[LogInterceptor] 日志拦截器已{(isLogInterceptorEnabled ? "启用" : "禁用")}"); + } + catch (Exception invokeEx) + { + Console.WriteLine($"[LogInterceptor] Initialize方法调用异常: {invokeEx.Message}"); + Console.WriteLine($"[LogInterceptor] 异常详情: {invokeEx.StackTrace}"); + } + // 初始化信息由LogStreamInterceptor内部输出 + } + else + { + Console.WriteLine("[LogInterceptor] 未找到Initialize方法"); + } + } + else + { + Console.WriteLine("[LogInterceptor] 未找到LogStreamInterceptor类型"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + Console.WriteLine($"[LogInterceptor] 异常详情: {ex.StackTrace}"); + } + + // 继续主程序逻辑... +} +``` + +### 2. 配置控制机制 + +#### 配置文件控制 + +LogStreamInterceptor通过PluginConfig.json中的StreamInterceptor配置项控制启用状态: + +```json +{ + "StreamInterceptor": { + "Enabled": true, + "InterceptLevel": "Info", + "LogDestination": "Console" + } +} +``` + +- `Enabled`: 控制LogStreamInterceptor是否启用 +- `InterceptLevel`: 日志拦截级别 +- `LogDestination`: 日志重定向目标 + +#### 配置检查逻辑 + +在Program.cs中的配置检查流程: + +1. **获取PluginManager类型**:通过反射获取PluginManager类 +2. **调用GetConfig方法**:获取当前插件配置 +3. **读取StreamInterceptor配置**:提取StreamInterceptor节的配置 +4. **检查Enabled属性**:读取Enabled标志 +5. **传递启用状态**:将启用状态传递给Initialize方法 + +#### 启用状态传递 + +```csharp +// 根据配置决定是否启用日志拦截器 +initializeMethod.Invoke(null, new object[] { isLogInterceptorEnabled }); +Console.WriteLine($"[LogInterceptor] 日志拦截器已{(isLogInterceptorEnabled ? "启用" : "禁用")}"); +``` + +### 3. 集成流程 + +在主程序启动过程中,LogStreamInterceptor的集成流程如下: + +1. **插件系统初始化**:首先初始化PluginManager和插件系统 +2. **配置检查**:读取PluginConfig.json中的StreamInterceptor配置 +3. **类型查找**:通过反射查找LogStreamInterceptor类型 +4. **方法调用**:调用Initialize方法传递启用状态 +5. **初始化完成**:根据启用状态决定是否初始化Console重定向 + +## 接口定义和实现 + +### 1. 插件日志级别枚举 + +```csharp +public enum PluginLogLevel +{ + Debug, + Info, + Warn, + Error, + Fatal +} +``` + +### 2. IStreamInterceptor接口定义 + +```csharp +public interface IStreamInterceptor +{ + // 输入流拦截 + string[] InterceptInput(string[] originalArgs); + object InterceptOptions(object originalOption); + + // 输出流拦截 + string InterceptOutput(string originalOutput, string outputType); + void OnOutputRedirect(string originalPath, string newPath); + + // 日志流拦截 + string InterceptLog(string originalLog, PluginLogLevel level); + void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination); +} +``` + +### 3. 日志拦截接口实现 + +```csharp +public class StreamInterceptorPlugin : IPlugin, IStreamInterceptor +{ + public string InterceptLog(string originalLog, PluginLogLevel level) + { + // 日志拦截处理逻辑 + var result = originalLog; + + // 根据日志级别进行不同处理 + switch (level) + { + case PluginLogLevel.Debug: + // 调试日志处理 + result = ProcessDebugLog(originalLog); + break; + case PluginLogLevel.Info: + // 信息日志处理 + result = ProcessInfoLog(originalLog); + break; + case PluginLogLevel.Warn: + // 警告日志处理 + result = ProcessWarnLog(originalLog); + break; + case PluginLogLevel.Error: + case PluginLogLevel.Fatal: + // 错误日志处理 + result = ProcessErrorLog(originalLog); + break; + } + + return result; + } + + public void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination) + { + // 日志重定向处理逻辑 + Console.WriteLine($"[LogRedirect] {originalLog} -> {newDestination}"); + + // 可以在这里执行重定向相关的操作 + UpdateLogDestination(originalLog, newDestination); + } +} +``` + +## 使用场景和示例 + +### 1. 日志过滤插件 + +```csharp +public class LogFilterPlugin : IPlugin, IStreamInterceptor +{ + private readonly HashSet _filteredKeywords = new HashSet + { + "DEBUG", "TRACE", "VERBOSE" + }; + + public string InterceptLog(string originalLog, PluginLogLevel level) + { + // 过滤包含敏感关键词的日志 + foreach (var keyword in _filteredKeywords) + { + if (originalLog.Contains(keyword) && level == PluginLogLevel.Debug) + { + return string.Empty; // 返回空字符串表示过滤掉 + } + } + + return originalLog; + } +} +``` + +### 2. 日志格式化插件 + +```csharp +public class LogFormatPlugin : IPlugin, IStreamInterceptor +{ + public string InterceptLog(string originalLog, PluginLogLevel level) + { + // 添加时间戳和级别标识 + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var levelPrefix = level.ToString().ToUpper().PadRight(5); + + return $"[{timestamp}] [{levelPrefix}] {originalLog}"; + } +} +``` + +### 3. 日志重定向插件 + +```csharp +public class LogRedirectPlugin : IPlugin, IStreamInterceptor +{ + private readonly Dictionary _logDestinations = new Dictionary + { + { PluginLogLevel.Error, "error.log" }, + { PluginLogLevel.Fatal, "fatal.log" }, + { PluginLogLevel.Warn, "warning.log" }, + { PluginLogLevel.Info, "info.log" }, + { PluginLogLevel.Debug, "debug.log" } + }; + + public void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination) + { + if (_logDestinations.ContainsKey(level)) + { + var fileName = _logDestinations[level]; + File.AppendAllText(fileName, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {originalLog}\n"); + } + } +} +``` + +### 4. 批量下载日志插件 + +```csharp +public class BatchDownloadLogPlugin : IPlugin, IStreamInterceptor +{ + private readonly List _logEntries = new List(); + + public string InterceptLog(string originalLog, PluginLogLevel level) + { + // 收集与批量下载相关的日志 + if (originalLog.Contains("BatchDownload") || originalLog.Contains("batch")) + { + _logEntries.Add($"[{level}] {originalLog}"); + } + + return originalLog; + } + + public void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination) + { + // 在批量下载完成后输出汇总日志 + if (originalLog.Contains("BatchDownload completed")) + { + Console.WriteLine($"\n=== 批量下载日志汇总 ==="); + foreach (var entry in _logEntries) + { + Console.WriteLine(entry); + } + Console.WriteLine("=== 批量下载日志汇总结束 ===\n"); + } + } +} +``` + +## 错误处理机制 + +### 1. 初始化错误处理 + +```csharp +public static void Initialize() +{ + try + { + // 初始化逻辑 + } + catch (Exception ex) + { + // 使用Debug输出错误信息,避免Console重定向问题 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + + try + { + // 尝试使用Console.WriteLine输出错误 + Console.WriteLine($"[LogInterceptor] 初始化失败: {ex.Message}"); + } + catch + { + // 如果Console不可用,忽略 + } + } +} +``` + +### 2. 拦截错误处理 + +```csharp +public static string InterceptLog(string originalLog, PluginLogLevel level) +{ + var result = originalLog; + + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptLog(result, level); + if (string.IsNullOrEmpty(result)) + result = originalLog; // 保护原始日志 + } + catch (Exception ex) + { + // 使用Debug输出错误信息,避免Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] 拦截器 {interceptor.GetType().Name} 处理错误: {ex.Message}"); + } + } + + return result; +} +``` + +### 3. 错误处理原则 + +- **防递归设计**:始终使用Debug.WriteLine输出错误信息 +- **原始数据保护**:出现错误时保留原始日志内容 +- **错误隔离**:单个拦截器错误不影响其他拦截器 +- **继续处理**:错误后继续执行后续拦截器 + +## 性能优化 + +### 1. 重定向优化 + +- **双重写入优化**:合理管理StringWriter实例 +- **字符串处理优化**:避免在拦截器中创建大量临时字符串 +- **缓冲区机制**:使用缓冲区减少I/O操作 + +### 2. 拦截器优化 + +- **早期退出**:如果拦截器列表为空,直接返回原始日志 +- **空值检查**:避免处理空日志消息 +- **高效链表**:使用List存储拦截器 + +### 3. 内存管理 + +- **及时释放**:拦截器处理完成后及时释放临时资源 +- **日志缓存**:合理管理日志缓存大小 +- **StringWriter复用**:避免频繁创建StringWriter实例 + +## 调试和监控 + +### 1. 调试输出 + +```csharp +public static void Initialize() +{ + // 【调试】记录Console.Out的类型 + var originalOutType = Console.Out.GetType().Name; + var originalErrorType = Console.Error.GetType().Name; + System.Diagnostics.Debug.WriteLine($"[LogInterceptor] Console.Out类型: {originalOutType}, Console.Error类型: {originalErrorType}"); +} +``` + +### 2. 调试技巧 + +- **Debug.WriteLine**:始终使用Debug输出避免触发Console拦截 +- **类型记录**:记录Console输出对象的类型信息 +- **初始化日志**:记录初始化过程的详细信息 + +### 3. 监控指标 + +- **拦截器数量**:监控注册的拦截器数量 +- **日志处理量**:监控处理的日志消息数量 +- **重定向次数**:监控Console输出重定向的次数 +- **错误率**:监控拦截器执行错误率 + +## 最佳实践 + +### 1. 拦截器开发 + +- **防递归**:始终使用Debug.WriteLine进行调试输出 +- **接口实现**:确保正确实现IStreamInterceptor接口 +- **日志保护**:确保拦截器不会返回null或空字符串 +- **异常处理**:正确处理可能出现的异常情况 +- **配置检查**:拦截器需要考虑LogStreamInterceptor的启用状态 + +### 2. 配置控制使用 + +- **启用控制**:通过PluginConfig.json中的StreamInterceptor.Enabled控制是否启用 +- **动态配置**:可以动态修改配置实现启用/禁用切换 +- **错误处理**:配置检查失败时使用默认设置(禁用) +- **状态传递**:确保启用状态正确传递给Initialize方法 + +### 3. 初始化使用 + +- **时机选择**:在程序早期初始化,避免遗漏日志 +- **错误处理**:妥善处理初始化失败的情况 +- **状态检查**:检查初始化是否成功 + +### 3. 日志处理 + +- **级别识别**:正确识别和处理不同级别的日志 +- **内容过滤**:合理过滤敏感或不需要的日志 +- **格式化**:提供一致的日志格式 + +## 故障排除 + +### 1. 常见问题 + +**初始化失败** +- 检查Console输出是否正常 +- 检查是否有权限创建StringWriter +- 查看Debug输出中的错误信息 + +**日志拦截不生效** +- 检查拦截器是否正确注册到PluginManager +- 检查拦截器是否正确实现IStreamInterceptor接口 +- 检查调用时机是否正确 + +**递归调用问题** +- 检查是否使用了Console.WriteLine进行调试输出 +- 检查拦截器实现是否正确处理递归调用 + +### 2. 调试步骤 + +1. **检查初始化**:确认LogStreamInterceptor.Initialize()被正确调用 +2. **检查注册**:确认拦截器已正确注册到LogStreamInterceptor +3. **检查调用**:确认Console输出被正确重定向 +4. **检查异常**:查看Debug输出中的错误信息 + +### 3. 修复建议 + +- **重新初始化**:重新调用Initialize()方法 +- **清理状态**:清理可能的状态冲突 +- **Console检查**:检查Console输出是否正常 + +## 扩展指南 + +### 1. 新增日志级别 + +1. **扩展枚举**:在PluginLogLevel枚举中添加新级别 +2. **处理逻辑**:为新级别实现特定的处理逻辑 +3. **类型注册**:在拦截器中注册新级别处理 + +### 2. 复杂重定向场景 + +**条件重定向** +- 根据日志内容或级别进行条件重定向 +- 支持多个重定向规则 +- 提供重定向规则配置 + +**动态重定向** +- 根据运行时状态动态生成重定向目标 +- 支持模板化的重定向路径 +- 提供重定向路径的验证机制 + +## 总结 + +LogStreamInterceptor作为N_m3u8DL-RE插件系统的日志流接管组件,提供了强大的Console输出重定向和日志拦截能力。通过与PluginManager的紧密集成,实现了统一、可扩展的日志流处理机制。 + +本文档详细介绍了LogStreamInterceptor的架构设计、功能实现、使用方式以及最佳实践,为开发者提供了完整的日志流拦截开发指南。通过合理使用LogStreamInterceptor,可以实现灵活的日志处理、过滤、重定向和格式化功能。 + +需要特别注意LogStreamInterceptor的防递归设计,确保在调试和错误处理时始终使用Debug.WriteLine,避免触发Console输出拦截导致的递归调用问题。 \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/Outputstreaminterceptor.cs\350\276\223\345\207\272\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/src/N_m3u8DL-RE/extend/extend-document/Outputstreaminterceptor.cs\350\276\223\345\207\272\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 00000000..3a81d35d --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/Outputstreaminterceptor.cs\350\276\223\345\207\272\346\265\201\346\216\245\347\256\241\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,608 @@ +# OutputStreamInterceptor.cs输出流接管插件开发文档 + +## 概述 + +OutputStreamInterceptor是N_m3u8DL-RE插件系统中的输出流接管组件,负责拦截和处理程序输出流,包括文件路径重定向、输出消息处理以及输出事件通知。本文档详细介绍了OutputStreamInterceptor的架构设计、核心功能、使用方式以及与PluginManager的集成关系。 + +## 核心架构设计 + +### 1. 整体架构 + +OutputStreamInterceptor采用静态类设计,作为输出流接管的实现层,与PluginManager形成统一的拦截管理体系: + +```csharp +// 【输出流拦截】由PluginManager.cs统一管理输出流拦截功能 +// 该类处理输出流拦截和重定向逻辑,确保输出不丢失且可被插件拦截 + +public class OutputStreamInterceptor +{ + private static List _interceptors = new List(); + private static StringWriter? _originalOutput; + private static bool _isInitialized = false; +} +``` + +### 2. 架构特点 + +- **静态单例模式**:所有拦截器共享同一个实例,保证拦截链的一致性 +- **初始化机制**:具有专门的初始化方法,确保输出重定向的安全性 +- **链式拦截机制**:支持多个拦截器的依次调用,形成拦截链 +- **PluginManager托管**:由PluginManager统一管理拦截器的注册和调用 +- **防递归设计**:使用Debug.WriteLine避免Console输出导致的递归调用 + +## 核心功能模块 + +### 1. 初始化机制 + +#### 初始化实现 + +```csharp +/// +/// 初始化输出流拦截器 +/// 【输出流拦截】由PluginManager.cs统一调用此方法进行初始化 +/// +public static void Initialize() +{ + if (_isInitialized) return; + + try + { + // 先输出初始化消息到实际控制台(避免被拦截) + Console.WriteLine("[OutputInterceptor] 输出流拦截器初始化开始..."); + + // 保存原始输出引用 + _originalOutput = new StringWriter(); + + _isInitialized = true; + + // 输出初始化完成消息到实际控制台 + Console.WriteLine("[OutputInterceptor] 输出流拦截器初始化完成"); + } + catch (Exception ex) + { + // 使用Debug输出错误信息 + System.Diagnostics.Debug.WriteLine($"[OutputInterceptor] 初始化失败: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"[OutputInterceptor] 异常详情: {ex.StackTrace}"); + } +} +``` + +#### 初始化流程 + +1. **重复检查**:检查是否已经初始化,避免重复初始化 +2. **消息输出**:先输出初始化消息到实际控制台(避免被拦截) +3. **资源保存**:保存原始输出引用以备恢复 +4. **状态标记**:设置初始化状态标记 +5. **错误处理**:使用Debug输出错误信息避免递归 + +### 2. 拦截器注册机制 + +#### 注册方法实现 + +```csharp +/// +/// 注册输出流拦截器 +/// 【输出流拦截】由PluginManager.cs统一管理拦截器注册 +/// +public static void RegisterInterceptor(IStreamInterceptor interceptor) +{ + if (!_interceptors.Contains(interceptor)) + { + _interceptors.Add(interceptor); + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[OutputInterceptor] 已注册输出流拦截器: {interceptor.GetType().Name}"); + } +} +``` + +#### 注册流程 + +1. **重复检查**:检查拦截器是否已存在,避免重复注册 +2. **添加拦截器**:将新拦截器添加到拦截器列表 +3. **调试输出**:使用Debug.WriteLine记录注册信息 +4. **防递归设计**:避免Console输出导致的递归调用 + +### 3. 输出消息拦截机制 + +#### 输出拦截实现 + +```csharp +/// +/// 拦截并处理输出消息 +/// 【输出流拦截】由PluginManager.cs统一调用此方法进行输出拦截处理 +/// +public static string InterceptOutput(string originalOutput, string outputType) +{ + if (string.IsNullOrEmpty(originalOutput)) + return originalOutput; + + var result = originalOutput; + + // 依次调用所有拦截器进行处理 + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptOutput(result, outputType); + if (string.IsNullOrEmpty(result)) + result = originalOutput; // 如果被拦截为空,保留原始输出 + } + catch (Exception ex) + { + _originalOutput?.WriteLine($"[OutputInterceptor] 拦截器 {interceptor.GetType().Name} 处理错误: {ex.Message}"); + } + } + + return result; +} +``` + +#### 拦截流程 + +1. **空值检查**:检查输出消息是否为空 +2. **链式处理**:依次调用所有注册拦截器的InterceptOutput方法 +3. **结果保护**:如果拦截器返回空值,保留原始输出 +4. **错误隔离**:单个拦截器错误不影响其他拦截器 +5. **结果返回**:返回经过所有拦截器处理的输出消息 + +### 4. 输出重定向机制 + +#### 重定向实现 + +```csharp +/// +/// 处理输出重定向事件 +/// 【输出流拦截】由PluginManager.cs统一调用此方法处理输出重定向 +/// +public static void OnOutputRedirect(string originalPath, string newPath) +{ + foreach (var interceptor in _interceptors) + { + try + { + interceptor.OnOutputRedirect(originalPath, newPath); + } + catch (Exception ex) + { + _originalOutput?.WriteLine($"[OutputInterceptor] 拦截器 {interceptor.GetType().Name} 重定向错误: {ex.Message}"); + } + } +} +``` + +#### 重定向流程 + +1. **事件接收**:接收输出重定向事件 +2. **链式通知**:依次通知所有拦截器的OnOutputRedirect方法 +3. **错误处理**:记录重定向过程中的错误 +4. **状态更新**:拦截器可以更新内部状态或配置 + +## 与PluginManager的集成关系 + +### 1. 统一管理架构 + +OutputStreamInterceptor作为输出流接管的实现层,由PluginManager统一管理: + +```csharp +public static void RegisterStreamInterceptor(IStreamInterceptor interceptor) +{ + if (!_streamInterceptors.Contains(interceptor)) + { + _streamInterceptors.Add(interceptor); + + // 统一注册到各个拦截器 + InputStreamInterceptor.RegisterInterceptor(interceptor); + OutputStreamInterceptor.RegisterInterceptor(interceptor); + LogStreamInterceptor.RegisterInterceptor(interceptor); + } +} +``` + +### 2. 事件通知机制 + +```csharp +/// +/// 通知插件处理输出事件 +/// +internal static void NotifyPluginsOnOutput(string outputPath, string outputType) +{ + foreach (var plugin in _plugins) + { + try + { + plugin.OnOutputGenerated(outputPath, outputType); + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] Output notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } +} +``` + +## 与主程序的集成 + +### 1. 下载器集成点 + +在SimpleDownloader.cs中集成输出拦截: + +```csharp +private void TriggerOutputInterceptor(string filePath) +{ + try + { + // 输出流拦截 + var interceptedPath = OutputStreamInterceptor.InterceptOutput(filePath, "file"); + + if (interceptedPath != filePath) + { + // 处理重定向的输出路径 + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var redirectOutputMethod = pluginManagerType.GetMethod("RedirectOutput", + BindingFlags.NonPublic | BindingFlags.Static); + if (redirectOutputMethod != null) + { + redirectOutputMethod.Invoke(null, new object[] { filePath, interceptedPath }); + } + } + } + + // 插件输出事件通知 + PluginManager.NotifyPluginsOnOutput(interceptedPath, "file"); + } + catch (Exception ex) + { + Logger.Warn($"[OutputInterceptor] Failed to process output redirection: {ex.Message}"); + } +} +``` + +### 2. 下载管理器集成 + +在SimpleDownloadManager.cs中集成输出路径处理: + +```csharp +private string InterceptFileOutput(string filePath, string outputType) +{ + try + { + // 输出流拦截 + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var notifyOutputMethod = pluginManagerType.GetMethod("NotifyPluginsOnOutput", + BindingFlags.NonPublic | BindingFlags.Static); + if (notifyOutputMethod != null) + { + notifyOutputMethod.Invoke(null, new object[] { filePath, outputType }); + } + } + + // 使用输出拦截器 + filePath = OutputStreamInterceptor.RedirectOutputPath(filePath, outputType); + } + catch (Exception ex) + { + Console.WriteLine($"[OutputInterceptor] Failed to process output: {ex.Message}"); + } + + return filePath; +} +``` + +## 接口定义和实现 + +### 1. IStreamInterceptor接口定义 + +```csharp +public interface IStreamInterceptor +{ + // 输入流拦截 + string[] InterceptInput(string[] originalArgs); + object InterceptOptions(object originalOption); + + // 输出流拦截 + string InterceptOutput(string originalOutput, string outputType); + void OnOutputRedirect(string originalPath, string newPath); + + // 日志流拦截 + string InterceptLog(string originalLog, PluginLogLevel level); + void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination); +} +``` + +### 2. 输出拦截接口实现 + +```csharp +public class StreamInterceptorPlugin : IPlugin, IStreamInterceptor +{ + public string InterceptOutput(string originalOutput, string outputType) + { + // 输出消息拦截逻辑 + var result = originalOutput; + + // 示例:根据输出类型进行不同处理 + switch (outputType.ToLower()) + { + case "file": + // 文件输出处理 + result = ProcessFileOutput(originalOutput); + break; + case "console": + // 控制台输出处理 + result = ProcessConsoleOutput(originalOutput); + break; + case "log": + // 日志输出处理 + result = ProcessLogOutput(originalOutput); + break; + } + + return result; + } + + public void OnOutputRedirect(string originalPath, string newPath) + { + // 输出重定向处理逻辑 + Console.WriteLine($"[OutputRedirect] {originalPath} -> {newPath}"); + + // 可以在这里执行重定向相关的操作 + UpdateFileMapping(originalPath, newPath); + } +} +``` + +## 使用场景和示例 + +### 1. 文件路径重定向插件 + +```csharp +public class FileRedirectPlugin : IPlugin, IStreamInterceptor +{ + private readonly Dictionary _pathMappings = new Dictionary(); + + public string InterceptOutput(string originalOutput, string outputType) + { + if (outputType == "file" && _pathMappings.ContainsKey(originalOutput)) + { + return _pathMappings[originalOutput]; + } + + return originalOutput; + } + + public void OnOutputRedirect(string originalPath, string newPath) + { + _pathMappings[originalPath] = newPath; + Console.WriteLine($"[FileRedirect] 路径重定向: {originalPath} -> {newPath}"); + } +} +``` + +### 2. 输出格式转换插件 + +```csharp +public class OutputFormatPlugin : IPlugin, IStreamInterceptor +{ + public string InterceptOutput(string originalOutput, string outputType) + { + switch (outputType.ToLower()) + { + case "file": + // 为文件输出添加时间戳 + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + return $"[{timestamp}] {originalOutput}"; + + case "console": + // 为控制台输出添加颜色标记 + return $"[CONSOLE] {originalOutput}"; + + default: + return originalOutput; + } + } +} +``` + +### 3. 批量下载输出插件 + +```csharp +public class BatchDownloadPlugin : IPlugin, IStreamInterceptor +{ + private int _downloadCount = 0; + + public string InterceptOutput(string originalOutput, string outputType) + { + if (outputType == "file" && originalOutput.Contains(".m3u8")) + { + _downloadCount++; + var baseName = Path.GetFileNameWithoutExtension(originalOutput); + var extension = Path.GetExtension(originalOutput); + + // 生成批量下载文件名 + var batchFileName = $"{baseName}_batch{_downloadCount:03d}{extension}"; + return Path.Combine(Path.GetDirectoryName(originalOutput)!, batchFileName); + } + + return originalOutput; + } + + public void OnOutputRedirect(string originalPath, string newPath) + { + // 记录批量下载的文件映射 + LogFileMapping(originalPath, newPath); + } +} +``` + +## 错误处理机制 + +### 1. 异常捕获策略 + +```csharp +public static string InterceptOutput(string originalOutput, string outputType) +{ + var result = originalOutput; + + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptOutput(result, outputType); + if (string.IsNullOrEmpty(result)) + result = originalOutput; // 保护原始输出 + } + catch (Exception ex) + { + _originalOutput?.WriteLine($"[OutputInterceptor] 拦截器 {interceptor.GetType().Name} 处理错误: {ex.Message}"); + } + } + + return result; +} +``` + +### 2. 错误处理原则 + +- **错误隔离**:单个拦截器错误不影响其他拦截器 +- **原始输出保护**:出现错误时保留原始输出 +- **错误记录**:详细记录错误信息便于调试 +- **继续处理**:错误后继续执行后续拦截器 + +### 3. 常见错误类型 + +1. **空值处理错误**:拦截器处理空输出消息 +2. **路径格式错误**:文件路径格式不正确 +3. **权限错误**:访问受限的文件或目录 +4. **字符编码错误**:输出内容编码问题 + +## 性能优化 + +### 1. 拦截器优化 + +- **早期退出**:如果拦截器列表为空,直接返回原始输出 +- **空值检查**:避免处理空输出消息 +- **高效链表**:使用List存储拦截器,支持快速遍历 +- **异常隔离**:使用try-catch隔离异常,避免性能损耗 + +### 2. 内存管理 + +- **StringWriter复用**:合理管理StringWriter实例 +- **字符串优化**:避免在拦截器中创建大量临时字符串 +- **及时释放**:拦截器处理完成后及时释放临时资源 + +### 3. 输出优化 + +- **批量处理**:对于大量输出,考虑批量处理 +- **缓冲机制**:使用缓冲区减少I/O操作 +- **异步处理**:对于耗时操作,考虑异步处理 + +## 调试和监控 + +### 1. 调试输出 + +```csharp +public static void RegisterInterceptor(IStreamInterceptor interceptor) +{ + if (!_interceptors.Contains(interceptor)) + { + _interceptors.Add(interceptor); + // 使用Debug输出避免触发Console输出拦截 + System.Diagnostics.Debug.WriteLine($"[OutputInterceptor] 已注册输出流拦截器: {interceptor.GetType().Name}"); + } +} +``` + +### 2. 调试技巧 + +- **Debug.WriteLine**:使用Debug输出避免触发Console拦截 +- **初始化日志**:记录初始化过程的详细信息 +- **错误日志**:记录拦截器执行过程中的错误 + +### 3. 监控指标 + +- **拦截器数量**:监控注册的拦截器数量 +- **处理次数**:监控输出拦截处理的次数 +- **错误率**:监控拦截器执行错误率 +- **处理时间**:监控拦截处理耗时 + +## 最佳实践 + +### 1. 拦截器开发 + +- **接口实现**:确保正确实现IStreamInterceptor接口 +- **输出保护**:确保拦截器不会返回null或空字符串 +- **异常处理**:正确处理可能出现的异常情况 +- **性能考虑**:避免在拦截器中进行耗时操作 + +### 2. 集成使用 + +- **PluginManager管理**:通过PluginManager统一管理拦截器 +- **注册顺序**:考虑拦截器的注册顺序对结果的影响 +- **状态管理**:避免拦截器之间的状态冲突 + +### 3. 重定向处理 + +- **路径验证**:验证重定向路径的有效性 +- **权限检查**:确保有权限访问重定向目标 +- **错误恢复**:提供重定向失败时的恢复机制 + +## 故障排除 + +### 1. 常见问题 + +**输出拦截不生效** +- 检查拦截器是否正确注册到PluginManager +- 检查拦截器是否正确实现IStreamInterceptor接口 +- 检查调用时机是否正确 + +**文件重定向失效** +- 检查拦截器的OnOutputRedirect方法实现 +- 检查是否有权限访问目标路径 +- 检查路径格式是否正确 + +**初始化失败** +- 检查Console输出是否正常 +- 检查是否有权限创建StringWriter +- 查看Debug输出中的错误信息 + +### 2. 调试步骤 + +1. **检查注册**:确认拦截器已正确注册到OutputStreamInterceptor +2. **检查初始化**:确认OutputStreamInterceptor.Initialize()被正确调用 +3. **检查调用**:确认OutputStreamInterceptor.InterceptOutput()被正确调用 +4. **检查异常**:查看Debug输出中的错误信息 + +### 3. 修复建议 + +- **重新初始化**:重新调用Initialize()方法 +- **清理状态**:清理可能的状态冲突 +- **权限检查**:检查文件访问权限 + +## 扩展指南 + +### 1. 新增输出类型 + +1. **扩展输出类型**:在interceptOutput方法中支持新的输出类型 +2. **类型处理**:为新输出类型实现特定的处理逻辑 +3. **类型注册**:在PluginManager中注册新输出类型 + +### 2. 复杂重定向场景 + +**条件重定向** +- 根据输出内容或环境条件进行重定向 +- 支持多个重定向规则 +- 提供重定向规则配置 + +**动态重定向** +- 根据运行时状态动态生成重定向路径 +- 支持模板化的路径生成 +- 提供重定向路径的验证机制 + +## 总结 + +OutputStreamInterceptor作为N_m3u8DL-RE插件系统的输出流接管组件,提供了强大的输出消息拦截和文件路径重定向能力。通过与PluginManager的紧密集成,实现了统一、可扩展的输出流处理机制。 + +本文档详细介绍了OutputStreamInterceptor的架构设计、功能实现、使用方式以及最佳实践,为开发者提供了完整的输出流拦截开发指南。通过合理使用OutputStreamInterceptor,可以实现灵活的文件输出处理和插件功能扩展。 \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/PluginConfig.json\346\230\257\346\217\222\344\273\266\351\205\215\347\275\256\350\257\264\346\230\216\344\271\246.md" "b/src/N_m3u8DL-RE/extend/extend-document/PluginConfig.json\346\230\257\346\217\222\344\273\266\351\205\215\347\275\256\350\257\264\346\230\216\344\271\246.md" new file mode 100644 index 00000000..1502d87b --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/PluginConfig.json\346\230\257\346\217\222\344\273\266\351\205\215\347\275\256\350\257\264\346\230\216\344\271\246.md" @@ -0,0 +1,356 @@ +{ + /* + ================================================================================ + N_m3u8DL-RE 插件系统配置文件 + ================================================================================ + 本配置文件用于控制N_m3u8DL-RE项目中各种插件的启用状态和行为参数。 + 请根据实际需求修改相应配置项。 + + 配置说明: + - Enabled: 控制插件是否启用(true=启用,false=禁用) + - 所有其他配置项仅在Enabled=true时生效 + ================================================================================ + */ + + /* + ================================================================================ + 1. UA切换插件 (UASwitcher) + ================================================================================ + 功能说明: + - 在下载过程中定期切换User-Agent,避免被目标网站识别为机器人 + - 每下载一定数量文件后自动切换到下一个User-Agent + - 有助于提高下载成功率,特别是对反爬虫策略严格的网站 + + 适用场景: + - 需要访问有反爬虫策略的网站 + - 大批量下载时避免被封IP + - 提高下载的稳定性和成功率 + + 注意事项: + - User-Agent列表中的UA应尽量真实和常用 + - 建议至少配置3-5个不同的User-Agent + - 切换频率不宜过于频繁,避免异常行为 + ================================================================================ + */ + "UASwitcher": { + "Enabled": true, // 启用UA切换功能 + + /* + UserAgents: User-Agent列表 + 作用:提供多个真实的浏览器User-Agent供切换使用 + 使用场景: + - 模拟不同浏览器和操作系统的访问 + - 避免使用相同User-Agent频繁访问目标网站 + - 提高访问的隐蔽性和成功率 + + 配置建议: + - 使用最新版本的浏览器UA + - 包含不同操作系统(Windows、macOS、Android等) + - 保持UA格式的一致性和真实性 + - 定期更新UA列表以保持有效性 + */ + "UserAgents": [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "Mozilla/5.0 (Linux; Android 10; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36" + ] + }, + + /* + ================================================================================ + 2. 代理切换插件 (ProxySwitcher) + ================================================================================ + 功能说明: + - 通过Clash API自动切换代理服务器 + - 每下载一定数量文件后自动切换到下一个可用代理 + - 有助于分散流量,提高下载稳定性 + + 适用场景: + - 需要通过代理访问目标网站 + - 网络环境不稳定,需要切换网络出口 + - 避免单点IP被封禁或限制 + + 注意事项: + - 需要Clash或兼容Clash API的代理工具 + - 确保Clash API服务正常运行 + - 代理列表应包含多个可用节点 + - 切换间隔需要根据实际网络情况调整 + ================================================================================ + */ + "ProxySwitcher": { + "Enabled": true, // 启用代理切换功能 + + /* + ClashApiUrl: Clash API接口地址 + 作用:指定Clash的RESTful API地址,用于获取和切换代理节点 + 使用场景: + - 自动化代理节点管理 + - 避免手动切换代理的繁琐操作 + - 实现智能负载均衡 + + 配置格式:http://[IP地址]:[端口号] + 常见配置: + - 默认本地Clash:http://127.0.0.1:9090 + - 远程Clash服务:http://[远程IP]:9090 + + 注意事项: + - 确保Clash的external-controller已启用 + - 端口号需要与Clash配置中的external-port保持一致 + - API地址需要网络可达性 + */ + "ClashApiUrl": "http://127.0.0.1:9090", + + /* + SwitchInterval: 代理切换间隔 + 作用:控制每下载多少个文件后切换一次代理 + 使用场景: + - 控制代理切换频率,避免过于频繁 + - 根据网络稳定性调整切换策略 + - 平衡代理使用的均匀性和稳定性 + + 配置建议: + - 1-3:适合网络环境不稳定的情况 + - 5-10:适合一般网络环境 + - 20+:适合网络环境较好的情况 + + 注意事项: + - 切换过于频繁可能影响下载效率 + - 切换过于稀少可能达不到分散流量的效果 + - 需要根据实际网络环境和目标网站限制调整 + */ + "SwitchInterval": 1 + }, + + /* + ================================================================================ + 3. 批量下载插件 (BatchDownload) + ================================================================================ + 功能说明: + - 支持同时下载多个URL的视频文件 + - 自动生成包含URL信息的唯一文件名 + - 支持并发下载和重试机制 + - 提供批量进度跟踪和错误处理 + + 适用场景: + - 需要下载多个视频文件的场景 + - 批量处理视频资源 + - 自动化视频下载任务 + - 离线下载队列管理 + + 注意事项: + - 确保批量文件路径正确且可访问 + - 输出目录需要有写入权限 + - 建议设置合理的重试次数 + - 并发下载数量不宜过高,避免网络过载 + ================================================================================ + */ + "BatchDownload": { + "Enabled": true, // 启用批量下载功能 + + /* + BatchFile: 批量URL文件路径 + 作用:指定包含待下载URL列表的文件路径 + 使用场景: + - 批量下载多个m3u8视频 + - 管理下载任务队列 + - 离线下载任务配置 + + 文件格式要求: + - 每行一个URL + - 支持#开头的注释行 + - 空行会被自动忽略 + - URL必须是有效的m3u8链接 + + 示例文件内容: + # 这是注释行,会被忽略 + https://example.com/video1.m3u8 + https://example.com/video2.m3u8 + # 另一个注释 + https://example.com/video3.m3u8 + + 注意事项: + - 文件编码建议使用UTF-8 + - 确保文件路径可访问 + - URL格式必须正确且有效 + */ + "BatchFile": "extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt", + + /* + MaxConcurrentDownloads: 最大并发下载数 + 作用:控制同时进行的下载任务数量 + 使用场景: + - 控制网络带宽使用 + - 避免同时下载过多文件导致系统负载过高 + - 根据网络环境调整下载策略 + + 配置建议: + - 1-2:适合网络环境较差或目标网站限制严格的情况 + - 3-5:适合一般网络环境 + - 6-10:适合网络环境较好且目标网站允许高并发的情况 + + 注意事项: + - 过高并发可能导致连接失败 + - 需要考虑目标网站的访问限制 + - 并发数过高可能消耗过多系统资源 + */ + "MaxConcurrentDownloads": 1, + + /* + OutputDirectory: 批量下载输出目录 + 作用:指定批量下载文件的保存目录 + 使用场景: + - 统一管理批量下载的文件 + - 区分不同批次的下载内容 + - 便于后续文件整理和处理 + + 配置格式: + - 相对路径:相对于程序运行目录 + - 绝对路径:完整的文件路径 + - 建议使用绝对路径避免路径解析问题 + + 示例: + - "extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin-output" + - "D:/Downloads/BatchDownloads" + - "/home/user/downloads/batch" + + 注意事项: + - 目录必须存在或可自动创建 + - 需要有写入权限 + - 建议使用独立的目录避免文件混乱 + */ + "OutputDirectory": "extend/BatchDownloadPlugin-and-input-output/BatchDownloadPlugin-output", + + /* + RetryCount: 重试次数 + 作用:控制单个下载任务失败后的重试次数 + 使用场景: + - 处理网络波动导致的下载失败 + - 提高下载成功率 + - 处理临时性服务器错误 + + 配置建议: + - 1-2:适合网络环境较好的情况 + - 3-5:适合一般网络环境 + - 5-10:适合网络环境较差或目标网站不稳定的情况 + + 注意事项: + - 重试次数过多可能延长总体下载时间 + - 需要考虑目标网站的访问限制 + - 建议结合适当的重试间隔 + */ + "RetryCount": 3, + + /* + CreateSubdirectories: 是否创建子目录 + 作用:控制是否为每个URL创建独立的子目录 + 使用场景: + - 区分不同来源的视频文件 + - 便于文件组织和分类管理 + - 避免文件名冲突 + + 配置选项: + - true:为每个URL创建独立子目录 + - false:所有文件保存在同一目录 + + 目录命名规则: + - 格式:{URL基础名}_batch{索引}_of_{总数} + - 示例:video1_batch1_of_5, video2_batch2_of_5 + + 注意事项: + - 创建子目录会增加文件系统操作 + - 目录层级过深可能影响文件访问 + - 需要确保有足够的文件系统权限 + */ + "CreateSubdirectories": false + }, + + /* + ================================================================================ + 4. 流拦截器插件 (StreamInterceptor) + ================================================================================ + 功能说明: + - 拦截和处理程序的各种输入输出流 + - 支持输入参数拦截和修改 + - 支持输出重定向和格式化 + - 支持日志拦截和过滤 + - 提供完整的流处理链 + + 适用场景: + - 自定义输入处理逻辑 + - 输出格式化和重定向 + - 日志收集和分析 + - 调试和监控程序行为 + + 注意事项: + - 流拦截可能影响程序性能 + - 需要确保拦截逻辑的稳定性 + - 避免无限循环和递归调用 + - 谨慎处理敏感信息 + ================================================================================ + */ + "StreamInterceptor": { + "Enabled": true, // 启用流拦截功能 + + /* + InterceptLevel: 拦截级别 + 作用:控制流拦截的详细程度和日志级别 + 使用场景: + - 调试和开发时的详细日志记录 + - 生产环境的简化日志输出 + - 特定错误场景的详细监控 + + 可选值及说明: + - "Debug": 最详细的拦截级别,包含所有调试信息 + 适用:开发调试、问题排查 + - "Info": 常规信息拦截,包含一般操作信息 + 适用:日常监控、操作记录 + - "Warning": 警告级别拦截,仅记录警告和错误 + 适用:生产环境、错误监控 + - "Error": 错误级别拦截,仅记录错误信息 + 适用:生产环境、错误统计 + + 配置建议: + - 开发环境:Debug + - 测试环境:Info或Warning + - 生产环境:Warning或Error + + 注意事项: + - 拦截级别越高,性能影响越大 + - Debug级别可能产生大量日志数据 + - 需要根据实际需求平衡详细度和性能 + */ + "InterceptLevel": "Info", + + /* + LogDestination: 日志输出目标 + 作用:指定拦截日志的输出位置和方式 + 使用场景: + - 调试时的控制台输出 + - 生产环境的日志文件记录 + - 集中化日志管理系统 + + 可选值及说明: + - "Console": 输出到控制台 + 适用:调试、开发、快速问题排查 + 特点:实时显示、便于查看 + - "File": 输出到日志文件 + 适用:生产环境、长期监控、审计需求 + 特点:持久化保存、便于分析 + - "Both": 同时输出到控制台和文件 + 适用:开发和生产兼顾的场景 + 特点:实时查看和持久化兼顾 + + 文件输出配置: + - 默认路径:程序运行目录下的logs文件夹 + - 文件命名:stream-interceptor-YYYYMMDD.log + - 文件轮转:按日期自动创建新日志文件 + + 注意事项: + - 控制台输出可能影响程序执行速度 + - 文件输出需要磁盘空间和写入权限 + - 大量日志可能影响磁盘性能 + - 需要定期清理历史日志文件 + */ + "LogDestination": "File" + } +} \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/PluginManager\346\217\222\344\273\266\347\256\241\347\220\206\345\231\250\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/src/N_m3u8DL-RE/extend/extend-document/PluginManager\346\217\222\344\273\266\347\256\241\347\220\206\345\231\250\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 00000000..137f8f7e --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/PluginManager\346\217\222\344\273\266\347\256\241\347\220\206\345\231\250\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,477 @@ +# PluginManager插件管理器开发文档 + +## 概述 + +PluginManager是N_m3u8DL-RE插件系统的核心组件,负责统一管理所有插件的生命周期、流拦截功能以及事件分发机制。本文档详细介绍了PluginManager的架构设计、核心功能、接口定义以及集成方式。 + +## 核心架构 + +### 1. 整体架构设计 + +PluginManager采用单一职责原则设计,主要职责包括: + +- **插件生命周期管理** - 插件的加载、初始化、注册和事件分发 +- **配置管理** - 统一的JSON配置文件解析和插件启用控制 +- **流拦截器统一管理** - 输入流、输出流、日志流的统一拦截和协调 +- **事件通知机制** - 输入、输出、日志事件的统一分发 +- **扩展接口定义** - IPlugin和IStreamInterceptor接口的标准化 + +### 2. 核心组件结构 + +```csharp +public static class PluginManager +{ + // 核心数据结构 + private static readonly List _plugins = new List(); + private static readonly List _streamInterceptors = new List(); + private static int _downloadCount = 0; + private static PluginConfig? _config; +} +``` + +## 核心功能模块 + +### 1. 插件管理系统 + +#### 插件加载机制 + +```csharp +public static void LoadPlugins() +{ + // 1. 加载配置文件 + LoadConfig(); + + // 2. 反射扫描插件类型 + var pluginTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.Namespace == "N_m3u8DL_RE.Plugin" && + t.Name.EndsWith("Plugin") && + !t.IsInterface && + !t.IsAbstract); + + // 3. 实例化和初始化插件 + foreach (var type in pluginTypes) + { + if (Activator.CreateInstance(type) is IPlugin plugin) + { + var pluginName = type.Name.Replace("Plugin", ""); + if (IsPluginEnabled(pluginName)) + { + plugin.Initialize(_config); + _plugins.Add(plugin); + + // 4. 注册流拦截器 + if (plugin is IStreamInterceptor interceptor) + { + RegisterStreamInterceptor(interceptor); + } + } + } + } +} +``` + +#### 配置管理机制 + +```csharp +private static void LoadConfig() +{ + var configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "extend", "PluginConfig.json"); + + // 手动JSON解析,避免反射限制 + if (json.Contains("\"UASwitcher\"")) + { + var uaEnabled = ExtractJsonBoolValue(json, "UASwitcher", "Enabled"); + _config.UASwitcher = new UASwitcherConfig { Enabled = uaEnabled }; + } + + if (json.Contains("\"StreamInterceptor\"")) + { + var streamInterceptorEnabled = ExtractJsonBoolValue(json, "StreamInterceptor", "Enabled"); + _config.StreamInterceptor = new StreamInterceptorConfig + { + Enabled = streamInterceptorEnabled, + InterceptLevel = ExtractJsonValue(json, "InterceptLevel", "Info"), + LogDestination = ExtractJsonValue(json, "LogDestination", "Console") + }; + } +} +``` + +### 2. 流拦截器管理系统 + +#### 统一拦截器注册 + +```csharp +public static void RegisterStreamInterceptor(IStreamInterceptor interceptor) +{ + if (!_streamInterceptors.Contains(interceptor)) + { + _streamInterceptors.Add(interceptor); + + // 统一注册到三个拦截器 + InputStreamInterceptor.RegisterInterceptor(interceptor); + OutputStreamInterceptor.RegisterInterceptor(interceptor); + LogStreamInterceptor.RegisterInterceptor(interceptor); + } +} +``` + +#### 事件通知机制 + +```csharp +// 输入事件通知 +internal static void NotifyPluginsOnInput(object args, object option) +{ + foreach (var plugin in _plugins) + { + plugin.OnInputReceived(args, option); + } +} + +// 输出事件通知 +internal static void NotifyPluginsOnOutput(string outputPath, string outputType) +{ + foreach (var plugin in _plugins) + { + plugin.OnOutputGenerated(outputPath, outputType); + } +} + +// 日志事件通知 +internal static void NotifyPluginsOnLog(string logMessage, PluginLogLevel logLevel) +{ + foreach (var plugin in _plugins) + { + plugin.OnLogGenerated(logMessage, logLevel); + } +} +``` + +## 接口定义规范 + +### 1. 基础插件接口 + +```csharp +public interface IPlugin +{ + // 核心生命周期方法 + void Initialize(PluginConfig? config); + void OnFileDownloaded(string filePath, int downloadCount); + + // 扩展事件方法 + void OnInputReceived(object args, object option); + void OnOutputGenerated(string outputPath, string outputType); + void OnLogGenerated(string logMessage, PluginLogLevel logLevel); +} +``` + +### 2. 流拦截器接口 + +```csharp +public interface IStreamInterceptor +{ + // 输入流拦截 + string[] InterceptInput(string[] originalArgs); + object InterceptOptions(object originalOption); + + // 输出流拦截 + string InterceptOutput(string originalOutput, string outputType); + void OnOutputRedirect(string originalPath, string newPath); + + // 日志流拦截 + string InterceptLog(string originalLog, PluginLogLevel level); + void OnLogRedirect(string originalLog, PluginLogLevel level, string newDestination); +} +``` + +### 3. 插件配置类 + +```csharp +public class PluginConfig +{ + public UASwitcherConfig? UASwitcher { get; set; } + public ProxySwitcherConfig? ProxySwitcher { get; set; } + public StreamInterceptorConfig? StreamInterceptor { get; set; } +} + +public class UASwitcherConfig +{ + public bool Enabled { get; set; } = false; + public List UserAgents { get; set; } = new List(); +} + +public class ProxySwitcherConfig +{ + public bool Enabled { get; set; } = false; + public string ClashApiUrl { get; set; } = "http://127.0.0.1:9090"; + public int SwitchInterval { get; set; } = 3; +} + +public class StreamInterceptorConfig +{ + public bool Enabled { get; set; } = false; + public string InterceptLevel { get; set; } = "Info"; + public string LogDestination { get; set; } = "Console"; +} +``` + +## 配置管理系统 + +### 1. 配置文件格式 + +```json +{ + "UASwitcher": { + "Enabled": true, + "UserAgents": [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + ] + }, + "ProxySwitcher": { + "Enabled": false, + "ClashApiUrl": "http://127.0.0.1:9090", + "SwitchInterval": 3 + }, + "StreamInterceptor": { + "Enabled": true, + "InterceptLevel": "Info", + "LogDestination": "Console" + }, + "BatchDownload": { + "Enabled": true, + "CreateSubdirectories": false + } +} +``` + +### 2. 配置解析机制 + +```csharp +private static bool ExtractJsonBoolValue(string json, string sectionName, string propertyName) +{ + var patterns = new[] + { + $"\\\"{sectionName}\\\":\\s*\\{{\"\\\"{propertyName}\\\":\\s*(true|false)", + $"\\\"{sectionName}\\\":\\s*\\{{\"\\\"{propertyName}\\\":\\s*(true|false)", + $"\\\"{sectionName}\\\":\\s*\\{{[^}}]*\\\"{propertyName}\\\":\\s*(true|false)" + }; + + foreach (var pattern in patterns) + { + var match = System.Text.RegularExpressions.Regex.Match(json, pattern); + if (match.Success) + { + return match.Groups[1].Value == "true"; + } + } + return false; +} +``` + +## 集成方式 + +### 1. 主程序集成 + +在Program.cs中初始化插件系统: + +```csharp +static async Task Main(string[] args) +{ + try + { + // 初始化插件系统 + PluginManager.LoadPlugins(); + + // 初始化流拦截器 + InputStreamInterceptor.Initialize(); + OutputStreamInterceptor.Initialize(); + LogStreamInterceptor.Initialize(); + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] 初始化失败: {ex.Message}"); + } + + // 继续主程序逻辑 + // ... +} +``` + +### 2. 命令行集成 + +在CommandInvoker.cs中集成输入拦截: + +```csharp +public void InvokeArgs(string[] args) +{ + try + { + // 输入流拦截 + args = InputStreamInterceptor.InterceptArgs(args); + + // 插件输入事件通知 + var notifyInputMethod = pluginManagerType.GetMethod("NotifyPluginsOnInput"); + if (notifyInputMethod != null) + { + notifyInputMethod.Invoke(null, new object[] { args, option }); + } + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Failed to process input: {ex.Message}"); + } +} +``` + +### 3. 下载器集成 + +在SimpleDownloader.cs中集成输出和日志拦截: + +```csharp +private void TriggerOutputInterceptor(string filePath) +{ + try + { + // 输出流拦截 + var interceptedPath = OutputStreamInterceptor.InterceptOutput(filePath, "file"); + + // 插件输出事件通知 + PluginManager.NotifyPluginsOnOutput(interceptedPath, "file"); + } + catch (Exception ex) + { + Logger.Warn($"[OutputInterceptor] Failed to process output: {ex.Message}"); + } +} +``` + +## 错误处理机制 + +### 1. 异常捕获策略 + +```csharp +internal static void NotifyPluginsOnOutput(string outputPath, string outputType) +{ + foreach (var plugin in _plugins) + { + try + { + plugin.OnOutputGenerated(outputPath, outputType); + } + catch (Exception ex) + { + Console.WriteLine($"[PluginManager] Output notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } +} +``` + +### 2. 配置错误处理 + +```csharp +private static void LoadConfig() +{ + try + { + // JSON解析逻辑 + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Manual config loading failed: {ex.Message}"); + _config = new PluginConfig(); // 使用默认配置 + } +} +``` + +## 性能优化 + +### 1. 反射优化 + +- 使用Assembly扫描时限定命名空间和类型名称 +- 缓存插件类型信息避免重复扫描 +- 使用Activator.CreateInstance减少反射开销 + +### 2. 配置解析优化 + +- 手动JSON解析避免序列化开销 +- 正则表达式缓存提高解析效率 +- 渐进式配置加载减少启动时间 + +### 3. 内存管理 + +- 使用单例模式管理PluginManager实例 +- 及时释放不需要的插件引用 +- 配置对象复用减少GC压力 + +## 扩展指南 + +### 1. 新增插件类型 + +1. 创建插件类实现IPlugin接口 +2. 在插件类名后添加"Plugin"后缀 +3. 在PluginConfig.json中添加相应配置 +4. PluginManager会自动发现和加载插件 + +### 2. 新增流拦截器 + +1. 创建拦截器类实现IStreamInterceptor接口 +2. 在插件初始化时调用PluginManager.RegisterStreamInterceptor() +3. 拦截器会自动注册到三个流拦截器中 + +### 3. 自定义配置 + +1. 在PluginConfig类中添加新配置属性 +2. 在LoadConfig方法中添加解析逻辑 +3. 在IsPluginEnabled方法中添加启用判断 + +## 最佳实践 + +### 1. 插件开发 + +- 插件应该实现IPlugin和IStreamInterceptor接口 +- 在Initialize方法中进行插件初始化 +- 在事件方法中处理业务逻辑,避免耗时操作 +- 正确处理异常,避免影响其他插件 + +### 2. 配置管理 + +- 配置文件的格式应该保持一致性 +- 使用合理的默认值避免配置错误 +- 配置验证应该在插件初始化时进行 + +### 3. 性能考虑 + +- 避免在拦截器中进行耗时操作 +- 使用异步处理提高性能 +- 合理使用缓存减少重复计算 + +## 故障排除 + +### 1. 常见问题 + +**插件未加载** +- 检查插件类名是否以"Plugin"结尾 +- 检查插件是否在正确命名空间中 +- 检查PluginConfig.json中的启用设置 + +**流拦截器不工作** +- 检查拦截器是否正确实现IStreamInterceptor接口 +- 检查是否调用了RegisterStreamInterceptor方法 +- 检查配置中是否启用了流拦截器 + +**配置加载失败** +- 检查PluginConfig.json文件是否存在 +- 检查JSON格式是否正确 +- 检查配置路径是否正确 + +### 2. 调试技巧 + +- 使用Debug.WriteLine进行调试输出,避免触发Console拦截 +- 查看插件加载日志确认插件状态 +- 使用配置验证确认配置正确性 + +## 总结 + +PluginManager作为N_m3u8DL-RE插件系统的核心,提供了完整的插件生命周期管理、流拦截功能以及事件分发机制。通过统一的接口设计和配置管理,为开发者提供了强大而灵活的插件扩展能力。 + +本文档涵盖了PluginManager的所有核心功能和使用方式,为插件开发和系统集成提供了完整的参考指南。 \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/PluginManager\346\217\222\344\273\266\347\256\241\347\220\206\345\231\250\345\274\200\345\217\221\346\255\245\351\252\244.md" "b/src/N_m3u8DL-RE/extend/extend-document/PluginManager\346\217\222\344\273\266\347\256\241\347\220\206\345\231\250\345\274\200\345\217\221\346\255\245\351\252\244.md" new file mode 100644 index 00000000..1406dd5d --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/PluginManager\346\217\222\344\273\266\347\256\241\347\220\206\345\231\250\345\274\200\345\217\221\346\255\245\351\252\244.md" @@ -0,0 +1,1147 @@ +# PluginManager插件管理器开发步骤 + +## 概述 + +本文档提供完整的开发步骤,用于实现插件管理器对N_m3u8DL-RE程序的输入流、输出流及日志输出流的全面接管机制。该方案遵循最小侵入原则,确保在不影响原有功能的前提下提供强大的插件扩展能力。 + +## 开发阶段规划 + +### 阶段1: 核心插件接口扩展 +### 阶段2: 输入流接管机制 +### 阶段3: 输出流接管机制 +### 阶段4: 日志流接管机制 +### 阶段5: 插件管理系统升级 +### 阶段6: 测试和验证 + +--- + +## 阶段1: 核心插件接口扩展 + +### 步骤1.1: 扩展IPlugin接口定义 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/PluginManager.cs` + +**操作**: 在现有IPlugin接口基础上添加新的接口方法 + +**代码修改**: +```csharp +public interface IPlugin +{ + // 原有方法保持不变 + void Initialize(PluginConfig? config); + void OnFileDownloaded(string filePath, int downloadCount); + + // 新增插件接口方法 + void OnInputReceived(string[] args, MyOption option); + void OnOutputGenerated(string outputPath, string outputType); + void OnLogGenerated(string logMessage, LogLevel logLevel); +} +``` + +### 步骤1.2: 创建流拦截器接口 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/PluginManager.cs` + +**操作**: 在IPlugin接口后添加流拦截器接口 + +**代码修改**: +```csharp +public interface IStreamInterceptor +{ + // 输入流拦截 + string[] InterceptInput(string[] originalArgs); + MyOption InterceptOptions(MyOption originalOption); + + // 输出流拦截 + string InterceptOutput(string originalOutput, string outputType); + void OnOutputRedirect(string originalPath, string newPath); + + // 日志流拦截 + string InterceptLog(string originalLog, LogLevel level); + void OnLogRedirect(string originalLog, LogLevel level, string newDestination); +} +``` + +### 步骤1.3: 创建日志级别枚举 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/PluginManager.cs` + +**操作**: 在命名空间内添加LogLevel枚举 + +**代码修改**: +```csharp +public enum LogLevel +{ + Debug, + Info, + Warn, + Error, + Fatal +} +``` + +--- + +### 🧪 阶段1测试验证 + +**操作**: 在完成阶段1所有步骤后进行测试验证 + +**验证目的**: 确保新接口定义不破坏现有插件系统 + +**测试步骤**: +```bash +# 1. 编译测试 +cd /workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE +dotnet build + +# 2. 验证插件加载 +dotnet run -- --help | grep -i plugin + +# 3. 检查BatchDownloadPlugin加载状态 +# 查看控制台输出中是否显示"Found X plugin types"和插件初始化信息 +``` + +**预期结果**: +- 编译成功无错误 +- 插件系统正常初始化 +- BatchDownloadPlugin等现有插件正常加载 + +**故障排除**: +- 如果编译错误,检查接口定义语法 +- 如果插件加载失败,检查命名空间和反射调用 + +## 阶段2: 输入流接管机制 + + +### 步骤2.1: 创建输入流拦截器 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/Interceptors/InputStreamInterceptor.cs` + +**操作**: 创建新的输入流拦截器类 + +**代码创建**: +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; + +namespace N_m3u8DL_RE.Plugin +{ + public class InputStreamInterceptor + { + private static List _interceptors = new List(); + + public static void RegisterInterceptor(IStreamInterceptor interceptor) + { + _interceptors.Add(interceptor); + } + + public static string[] InterceptArgs(string[] originalArgs) + { + var result = originalArgs; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptInput(result); + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; + } + + public static MyOption InterceptOptions(MyOption originalOption) + { + var result = originalOption; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptOptions(result); + } + catch (Exception ex) + { + Console.WriteLine($"[InputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; + } + } +} +``` + +### 步骤2.2: 修改CommandInvoker集成输入拦截 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs` + +**操作**: 在InvokeArgs方法开始处添加输入拦截逻辑 + +**代码修改**: +```csharp +// 在InvokeArgs方法开头添加 +try +{ + // 输入流拦截 + args = InputStreamInterceptor.InterceptArgs(args); + + // 插件输入事件通知 + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var notifyInputMethod = pluginManagerType.GetMethod("NotifyPluginsOnInput", + BindingFlags.NonPublic | BindingFlags.Static); + if (notifyInputMethod != null) + { + // 解析参数用于通知 + var rootCommand = new RootCommand(VERSION_INFO); + var parser = new CommandLineBuilder(rootCommand) + .UseDefaults() + .Build(); + + var parseResult = parser.Parse(args); + if (parseResult.GetValueForOption(Input) is MyOption option) + { + notifyInputMethod.Invoke(null, new object[] { args, option }); + } + } + } +} +catch (Exception ex) +{ + Console.WriteLine($"[InputInterceptor] Failed to process input: {ex.Message}"); +} +``` + +--- + + +### 🧪 阶段2测试验证 + +**操作**: 在完成阶段2所有步骤后进行测试验证 + +**验证目的**: 验证输入流拦截功能是否正常工作,使用BatchDownloadPlugin作为测试载体 + +**测试准备**: +```bash +# 创建测试URL文件 +echo "https://example.com/test1.m3u8" > extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt +echo "https://example.com/test2.m3u8" >> extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt +``` + +**测试步骤**: +```bash +# 1. 编译测试 +dotnet build + +# 2. 测试输入拦截功能 +dotnet run -- --batch --save-dir /tmp/test-input-intercept + +# 3. 验证输入拦截日志 +# 检查控制台输出中是否显示"[InputInterceptor]"相关日志信息 + +# 4. 测试参数解析 +dotnet run -- --help | head -5 +``` + +**预期结果**: +- 编译成功 +- 控制台显示输入拦截相关的日志信息 +- BatchDownloadPlugin正常处理批量参数 +- 命令行参数解析功能正常 + +**故障排除**: +- 如果无输入拦截日志,检查InputStreamInterceptor初始化 +- 如果BatchDownloadPlugin失效,检查CommandInvoker中的反射调用 + +## 阶段3: 输出流接管机制(开发完要注释说明是PluginManager.cs接管) + + +### 步骤3.1: 创建输出流拦截器 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/Interceptors/OutputStreamInterceptor.cs` + +**操作**: 创建新的输出流拦截器类 + +**代码创建**: +```csharp +using System; +using System.Collections.Generic; +using System.IO; + +namespace N_m3u8DL_RE.Plugin +{ + public class OutputStreamInterceptor + { + private static List _interceptors = new List(); + + public static void RegisterInterceptor(IStreamInterceptor interceptor) + { + _interceptors.Add(interceptor); + } + + public static string InterceptOutput(string originalOutput, string outputType) + { + var result = originalOutput; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptOutput(result, outputType); + } + catch (Exception ex) + { + Console.WriteLine($"[OutputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; + } + + public static string RedirectOutputPath(string originalPath, string outputType) + { + foreach (var interceptor in _interceptors) + { + try + { + interceptor.OnOutputRedirect(originalPath, originalPath); + } + catch (Exception ex) + { + Console.WriteLine($"[OutputInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return originalPath; + } + } +} +``` + +### 步骤3.2: 修改SimpleDownloadManager集成输出拦截 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs` + +**操作**: 在文件输出相关方法中添加输出拦截逻辑 + +**代码修改**: +```csharp +// 在文件保存相关方法中添加 +private string InterceptFileOutput(string filePath, string outputType) +{ + try + { + // 输出流拦截 + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var notifyOutputMethod = pluginManagerType.GetMethod("NotifyPluginsOnOutput", + BindingFlags.NonPublic | BindingFlags.Static); + if (notifyOutputMethod != null) + { + notifyOutputMethod.Invoke(null, new object[] { filePath, outputType }); + } + } + + // 使用输出拦截器 + filePath = OutputStreamInterceptor.RedirectOutputPath(filePath, outputType); + } + catch (Exception ex) + { + Console.WriteLine($"[OutputInterceptor] Failed to process output: {ex.Message}"); + } + + return filePath; +} +``` + +### 步骤3.3: 修改SimpleDownloader集成输出拦截 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs` + +**操作**: 在文件保存完成后添加输出拦截逻辑 + +**代码修改**: +```csharp +// 在现有TriggerPluginEvent方法后添加 +private void TriggerOutputInterceptor(string filePath) +{ + try + { + var interceptedPath = OutputStreamInterceptor.InterceptOutput(filePath, "file"); + + if (interceptedPath != filePath) + { + // 处理重定向的输出路径 + var pluginManagerType = Type.GetType("N_m3u8DL_RE.Plugin.PluginManager, N_m3u8DL-RE"); + if (pluginManagerType != null) + { + var redirectOutputMethod = pluginManagerType.GetMethod("RedirectOutput", + BindingFlags.NonPublic | BindingFlags.Static); + if (redirectOutputMethod != null) + { + redirectOutputMethod.Invoke(null, new object[] { filePath, interceptedPath }); + } + } + } + } + catch (Exception ex) + { + Logger.Warn($"[OutputInterceptor] Failed to process output redirection: {ex.Message}"); + } +} +``` + +--- + +### 🧪 阶段3测试验证 + +**操作**: 在完成阶段3所有步骤后进行测试验证 + +**验证目的**: 验证输出流拦截和重定向功能,使用BatchDownloadPlugin验证文件输出处理 + +**测试准备**: +```bash +# 创建测试目录 +mkdir -p /tmp/test-output-intercept + +# 确保有有效的测试URL +echo "https://sample-videos.com/zip/10/mp4/SampleVideo_1280x720_1mb.mp4" > extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt +``` + +**测试步骤**: +```bash +# 1. 编译测试 +dotnet build + +# 2. 测试输出拦截功能(如果网络可用,测试实际下载) +dotnet run -- --batch --save-dir /tmp/test-output-intercept --no-proxy + +# 3. 验证输出拦截日志 +# 检查控制台输出中是否显示"[OutputInterceptor]"相关日志信息 + +# 4. 检查文件输出处理 +ls -la /tmp/test-output-intercept/ + +# 5. 测试输出重定向(如果有StreamInterceptorPlugin) +dotnet run -- --batch --save-dir /tmp/test-output-redirect 2>&1 | grep -i "output.*intercept" +``` + +**预期结果**: +- 编译成功 +- 控制台显示输出拦截相关的日志信息 +- 文件输出路径处理正常 +- BatchDownloadPlugin生成的文件名符合预期格式 + +**故障排除**: +- 如果无输出拦截日志,检查OutputStreamInterceptor集成 +- 如果文件保存失败,检查SimpleDownloadManager的拦截调用 +- 如果输出重定向失效,检查PluginManager的RedirectOutput方法 + +## 阶段4: 日志流接管机制(开发完要注释说明是PluginManager.cs接管) + +### 步骤4.1: 创建日志流拦截器 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/Interceptors/LogStreamInterceptor.cs` + +**操作**: 创建新的日志流拦截器类 + +**代码创建**: +```csharp +using System; +using System.Collections.Generic; +using System.IO; + +namespace N_m3u8DL_RE.Plugin +{ + public class LogStreamInterceptor + { + private static List _interceptors = new List(); + private static StringWriter _originalConsoleOut; + private static StringWriter _originalConsoleError; + + public static void Initialize() + { + _originalConsoleOut = Console.Out; + _originalConsoleError = Console.Error; + + // 重定向Console输出 + var interceptedOut = new InterceptedStringWriter(_originalConsoleOut, "stdout"); + var interceptedErr = new InterceptedStringWriter(_originalConsoleError, "stderr"); + + Console.SetOut(interceptedOut); + Console.SetError(interceptedErr); + } + + public static void RegisterInterceptor(IStreamInterceptor interceptor) + { + _interceptors.Add(interceptor); + } + + public static string InterceptLog(string originalLog, LogLevel level) + { + var result = originalLog; + foreach (var interceptor in _interceptors) + { + try + { + result = interceptor.InterceptLog(result, level); + } + catch (Exception ex) + { + _originalConsoleOut.WriteLine($"[LogInterceptor] Error in {interceptor.GetType().Name}: {ex.Message}"); + } + } + return result; + } + + public static void Restore() + { + Console.SetOut(_originalConsoleOut); + Console.SetError(_originalConsoleError); + } + } + + public class InterceptedStringWriter : StringWriter + { + private readonly StringWriter _original; + private readonly string _streamType; + + public InterceptedStringWriter(StringWriter original, string streamType) + { + _original = original; + _streamType = streamType; + } + + public override void Write(string value) + { + var intercepted = LogStreamInterceptor.InterceptLog(value, LogLevel.Info); + _original.Write(intercepted); + base.Write(intercepted); + } + + public override void WriteLine(string value) + { + var intercepted = LogStreamInterceptor.InterceptLog(value, LogLevel.Info); + _original.WriteLine(intercepted); + base.WriteLine(intercepted); + } + } +} +``` + +### 步骤4.2: 修改Program.cs集成日志拦截 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/Program.cs` + +**操作**: 在Main方法开始处初始化日志拦截器 + +**代码修改**: +```csharp +static async Task Main(string[] args) +{ + // 初始化日志拦截器 + try + { + var logInterceptorType = Type.GetType("N_m3u8DL_RE.Plugin.LogStreamInterceptor, N_m3u8DL-RE"); + if (logInterceptorType != null) + { + var initializeMethod = logInterceptorType.GetMethod("Initialize"); + if (initializeMethod != null) + { + initializeMethod.Invoke(null, null); + Console.WriteLine("[LogInterceptor] Log stream interception initialized"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[LogInterceptor] Failed to initialize: {ex.Message}"); + } + + // 初始化插件系统(原有代码保持不变) + // ... +} +``` + +--- + +### 🧪 阶段4测试验证 + +**操作**: 在完成阶段4所有步骤后进行测试验证 + +**验证目的**: 验证日志流拦截和重定向功能,验证日志不丢失且正常显示 + +**测试准备**: +```bash +# 创建测试目录 +mkdir -p /tmp/test-log-intercept + +# 清理之前的测试文件 +rm -rf /tmp/test-log-intercept/* +``` + +**测试步骤**: +```bash +# 1. 编译测试 +dotnet build + +# 2. 测试日志拦截功能 +dotnet run -- --batch --save-dir /tmp/test-log-intercept 2>&1 | tee /tmp/log-test-output.txt + +# 3. 验证日志拦截日志 +grep -i "LogInterceptor" /tmp/log-test-output.txt + +# 4. 验证日志输出完整性 +# 检查控制台是否正常显示所有日志信息 +# 确认没有日志丢失或乱码 + +# 5. 测试不同日志级别 +dotnet run -- --batch --save-dir /tmp/test-log-intercept --debug 2>&1 | head -20 +``` + +**预期结果**: +- 编译成功 +- 控制台显示"[LogInterceptor]"初始化信息 +- 所有日志信息正常显示,无丢失 +- 日志拦截器正常工作但不干扰正常日志流程 + +**故障排除**: +- 如果日志显示异常,检查LogStreamInterceptor的Console重定向 +- 如果出现乱码,检查InterceptedStringWriter的实现 +- 如果性能下降,检查日志拦截的效率 + +## 阶段5: 插件管理系统升级(开发完要注释说明是PluginManager.cs接管) + + +### 步骤5.1: 升级PluginManager核心功能 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/PluginManager.cs` + +**操作**: 添加流拦截器管理和事件通知方法 + +**代码修改**: +```csharp +public static class PluginManager +{ + private static List _plugins = new List(); + private static List _streamInterceptors = new List(); + + // 新增流拦截器管理方法 + public static void RegisterStreamInterceptor(IStreamInterceptor interceptor) + { + _streamInterceptors.Add(interceptor); + + // 注册到各个拦截器 + InputStreamInterceptor.RegisterInterceptor(interceptor); + OutputStreamInterceptor.RegisterInterceptor(interceptor); + LogStreamInterceptor.RegisterInterceptor(interceptor); + } + + // 新增插件事件通知方法 + internal static void NotifyPluginsOnInput(string[] args, MyOption option) + { + foreach (var plugin in _plugins) + { + try + { + plugin.OnInputReceived(args, option); + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Input notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } + } + + internal static void NotifyPluginsOnOutput(string outputPath, string outputType) + { + foreach (var plugin in _plugins) + { + try + { + plugin.OnOutputGenerated(outputPath, outputType); + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Output notification failed for {plugin.GetType().Name}: {ex.Message}"); + } + } + } + + internal static void RedirectOutput(string originalPath, string newPath) + { + foreach (var plugin in _plugins) + { + try + { + plugin.OnOutputGenerated(newPath, "redirected"); + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Output redirection failed for {plugin.GetType().Name}: {ex.Message}"); + } + } + } + + // 修改LoadPlugins方法以支持流拦截器 + public static void LoadPlugins() + { + try + { + LoadConfig(); + + var pluginTypes = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.Namespace == "N_m3u8DL_RE.Plugin" && + t.Name.EndsWith("Plugin") && + !t.IsInterface && + !t.IsAbstract); + + Console.WriteLine($"[Plugin] Found {pluginTypes.Count()} plugin types"); + + foreach (var type in pluginTypes) + { + try + { + Console.WriteLine($"[Plugin] Creating instance of: {type.FullName}"); + var instance = Activator.CreateInstance(type); + + if (instance is IPlugin plugin) + { + var pluginName = type.Name.Replace("Plugin", ""); + var isEnabled = IsPluginEnabled(pluginName); + + Console.WriteLine($"[Plugin] Plugin {pluginName} enabled: {isEnabled}"); + + if (isEnabled) + { + plugin.Initialize(_config); + _plugins.Add(plugin); + + // 检查是否实现流拦截器接口 + if (instance is IStreamInterceptor interceptor) + { + RegisterStreamInterceptor(interceptor); + Console.WriteLine($"[Plugin] Registered stream interceptor: {pluginName}"); + } + + Console.WriteLine($"[Plugin] Loaded plugin: {pluginName}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] Failed to create instance of {type.Name}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[Plugin] LoadPlugins failed: {ex.Message}"); + } + } +} +``` + +### 步骤5.2: 创建示例流拦截器插件 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/Interceptors/StreamInterceptorPlugin.cs` + +**操作**: 创建示例插件演示流拦截功能 + +**代码创建**: +```csharp +using System; +using System.IO; + +namespace N_m3u8DL_RE.Plugin +{ + public class StreamInterceptorPlugin : IPlugin, IStreamInterceptor + { + private PluginConfig? _config; + + public void Initialize(PluginConfig? config) + { + _config = config; + Console.WriteLine("[StreamInterceptorPlugin] Initialized"); + } + + public void OnFileDownloaded(string filePath, int downloadCount) + { + // 原有文件下载事件处理 + } + + // IPlugin 新接口实现 + public void OnInputReceived(string[] args, MyOption option) + { + Console.WriteLine($"[StreamInterceptorPlugin] Input received: {args.Length} arguments"); + } + + public void OnOutputGenerated(string outputPath, string outputType) + { + Console.WriteLine($"[StreamInterceptorPlugin] Output generated: {outputPath} ({outputType})"); + } + + public void OnLogGenerated(string logMessage, LogLevel logLevel) + { + Console.WriteLine($"[StreamInterceptorPlugin] Log generated: {logLevel} - {logMessage}"); + } + + // IStreamInterceptor 接口实现 + public string[] InterceptInput(string[] originalArgs) + { + Console.WriteLine($"[StreamInterceptorPlugin] Intercepting {originalArgs.Length} input arguments"); + return originalArgs; + } + + public MyOption InterceptOptions(MyOption originalOption) + { + Console.WriteLine("[StreamInterceptorPlugin] Intercepting options"); + return originalOption; + } + + public string InterceptOutput(string originalOutput, string outputType) + { + var intercepted = $"[StreamInterceptorPlugin] {originalOutput}"; + Console.WriteLine($"[StreamInterceptorPlugin] Intercepted output: {outputType}"); + return intercepted; + } + + public void OnOutputRedirect(string originalPath, string newPath) + { + Console.WriteLine($"[StreamInterceptorPlugin] Output redirected: {originalPath} -> {newPath}"); + } + + public string InterceptLog(string originalLog, LogLevel level) + { + var intercepted = $"[StreamInterceptorPlugin] {originalLog}"; + return intercepted; + } + + public void OnLogRedirect(string originalLog, LogLevel level, string newDestination) + { + Console.WriteLine($"[StreamInterceptorPlugin] Log redirected: {level} to {newDestination}"); + } + } +} +``` + +### 步骤5.3: 更新PluginConfig.json配置 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/extend/PluginConfig.json` + +**操作**: 添加新插件配置项 + +**代码修改**: +```json +{ + "UASwitcher": { + "Enabled": true, + "UserAgents": [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" + ], + "SwitchInterval": 3 + }, + "ProxySwitcher": { + "Enabled": true, + "ClashApiUrl": "http://127.0.0.1:9090", + "SwitchInterval": 3 + }, + "BatchDownload": { + "Enabled": true, + "CreateSubdirectories": false, + "MaxConcurrency": 3 + }, + "StreamInterceptor": { + "Enabled": true, + "InterceptInput": true, + "InterceptOutput": true, + "InterceptLog": true, + "LogRedirection": false, + "OutputRedirection": false + } +} +``` + +--- + +### 🧪 阶段5测试验证 + +**操作**: 在完成阶段5所有步骤后进行测试验证 + +**验证目的**: 验证插件管理系统升级后的功能完整性和兼容性 + +**测试准备**: +```bash +# 创建综合测试目录 +mkdir -p /tmp/test-stage5-complete + +# 更新配置文件,确保StreamInterceptor插件启用 +cp extend/PluginConfig.json /tmp/test-stage5-complete/ +``` + +**测试步骤**: +```bash +# 1. 编译测试 +dotnet build + +# 2. 测试插件系统完整初始化 +dotnet run -- --help 2>&1 | grep -E "(Plugin|Found.*plugin)" + +# 3. 测试流拦截器注册 +dotnet run -- --batch --save-dir /tmp/test-stage5-complete 2>&1 | grep -E "(StreamInterceptor|Register.*interceptor)" + +# 4. 测试BatchDownloadPlugin与流拦截器协同工作 +echo "https://httpbin.org/stream/5" > extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt +dotnet run -- --batch --save-dir /tmp/test-stage5-complete --timeout 30 + +# 5. 验证所有插件事件通知 +dotnet run -- --batch --save-dir /tmp/test-stage5-complete 2>&1 | grep -E "(Input.*received|Output.*generated|Log.*generated)" +``` + +**预期结果**: +- 编译成功,所有新功能正常编译 +- 插件系统显示"Found X plugin types"和具体插件加载信息 +- 流拦截器成功注册并显示相关日志 +- BatchDownloadPlugin与流拦截器协同工作正常 +- 所有插件事件通知正常触发 + +**故障排除**: +- 如果插件加载失败,检查PluginManager的LoadPlugins方法 +- 如果流拦截器未注册,检查RegisterStreamInterceptor调用 +- 如果事件通知失效,检查NotifyPluginsOnXxx方法 + +## 阶段6: 测试和验证 + +### 步骤6.1: 编译测试 + +**操作**: 编译整个项目验证所有修改 + +**命令**: +```bash +cd /workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE +dotnet build +``` + +### 步骤6.2: 功能测试 + +**操作**: 运行基本功能测试 + +**命令**: +```bash +dotnet run -- --help +``` + +### 步骤6.3: 插件系统测试 + +**操作**: 测试插件加载和基本功能 + +**命令**: +```bash +dotnet run -- --batch --save-dir /tmp/test-output +``` + +### 步骤6.4: 流拦截测试 + +**操作**: 验证流拦截器是否正常工作 + +**验证点**: +- 输入参数拦截显示 +- 输出文件路径拦截显示 +- 日志输出拦截显示 +- 插件事件通知正常 + +### 🧪 阶段6综合测试验证 + +**操作**: 完成所有开发阶段后的最终综合测试 + +**验证目的**: 验证插件管理器完整接管(劫持)功能,确保与现有系统完全兼容 + +**测试准备**: +```bash +# 创建最终测试环境 +mkdir -p /tmp/final-integration-test +cd /tmp/final-integration-test + +# 准备完整的测试配置 +cat > test-config.json << 'EOF' +{ + "BatchDownload": { + "Enabled": true, + "CreateSubdirectories": false, + "MaxConcurrency": 2 + }, + "StreamInterceptor": { + "Enabled": true, + "InterceptInput": true, + "InterceptOutput": true, + "InterceptLog": true + } +} +EOF + +# 创建多个测试URL +cat > test-urls.txt << 'EOF' +https://httpbin.org/stream/3 +https://httpbin.org/json +EOF +``` + +**综合测试步骤**: +```bash +# 1. 完整编译验证 +dotnet build --configuration Release + +# 2. 启动模式测试 +echo "=== 测试1: 启动和插件加载 ===" +dotnet run -- --help 2>&1 | grep -E "(Plugin|Found.*plugin|StreamInterceptor)" + +# 3. 输入流接管测试 +echo "=== 测试2: 输入流接管 ===" +cd /tmp/final-integration-test +dotnet run /workspace/N_m3u8DL-RE-src -- --batch --save-dir /tmp/final-integration-test/output --timeout 20 2>&1 | tee input-test.log | grep -E "(InputInterceptor|Input.*received)" + +# 4. 输出流接管测试 +echo "=== 测试3: 输出流接管 ===" +dotnet run /workspace/N_m3u8DL-RE-src -- --batch --save-dir /tmp/final-integration-test/output2 --timeout 20 2>&1 | tee output-test.log | grep -E "(OutputInterceptor|Output.*generated)" + +# 5. 日志流接管测试 +echo "=== 测试4: 日志流接管 ===" +dotnet run /workspace/N_m3u8DL-RE-src -- --batch --save-dir /tmp/final-integration-test/output3 --debug --timeout 20 2>&1 | tee log-test.log | grep -E "(LogInterceptor|Log.*generated)" + +# 6. 兼容性测试(无插件模式) +echo "=== 测试5: 向后兼容性 ===" +# 临时禁用插件测试 +mv extend/PluginConfig.json extend/PluginConfig.json.backup +dotnet run /workspace/N_m3u8DL-RE-src -- --help > /dev/null 2>&1 && echo "兼容性测试通过" || echo "兼容性测试失败" +mv extend/PluginConfig.json.backup extend/PluginConfig.json +``` + +**最终验证检查清单**: +```bash +echo "=== 最终验证检查清单 ===" + +# 检查编译状态 +if dotnet build > /dev/null 2>&1; then + echo "✅ 编译测试通过" +else + echo "❌ 编译测试失败" +fi + +# 检查插件系统 +if dotnet run -- --help 2>&1 | grep -q "Found.*plugin"; then + echo "✅ 插件系统正常" +else + echo "❌ 插件系统异常" +fi + +# 检查流拦截器 +if dotnet run -- --batch --save-dir /tmp/quick-test 2>&1 | grep -q "StreamInterceptor"; then + echo "✅ 流拦截器正常" +else + echo "❌ 流拦截器异常" +fi + +# 检查BatchDownloadPlugin +if [ -f extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt ]; then + echo "✅ BatchDownloadPlugin输入文件存在" +else + echo "❌ BatchDownloadPlugin输入文件缺失" +fi + +# 检查配置文件 +if [ -f extend/PluginConfig.json ]; then + echo "✅ 配置文件存在" +else + echo "❌ 配置文件缺失" +fi + +# 检查输出目录 +if [ -d "/tmp/final-integration-test/output" ]; then + echo "✅ 输出目录创建正常" +else + echo "❌ 输出目录创建异常" +fi +``` + +**预期综合结果**: +- 所有编译测试通过,无错误和警告 +- 插件系统正常加载,显示具体插件信息 +- 输入流、输出流、日志流拦截功能全部正常 +- BatchDownloadPlugin与流拦截器协同工作 +- 向后兼容性测试通过,原有功能不受影响 +- 所有检查清单项目显示✅ + +**完整系统接管验证**: +通过以下命令验证插件管理器是否成功接管所有关键流程: +```bash +echo "=== 系统接管验证 ===" +dotnet run /workspace/N_m3u8DL-RE-src -- --batch --save-dir /tmp/system-hijack-test --timeout 15 2>&1 | \ +tee /tmp/final-hijack-test.log | \ +grep -E "(PluginManager|StreamInterceptor|InputInterceptor|OutputInterceptor|LogInterceptor|Input.*received|Output.*generated|Log.*generated)" | \ +sort | uniq -c +``` + +**如果此命令输出包含所有类型的拦截器日志,则证明插件管理器成功接管了系统的输入、输出和日志流程** + +--- + +## 配置文件管理 + +### 项目配置更新 + +**文件路径**: `/workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj` + +**操作**: 确保新文件被包含在项目中 + +**代码修改**: +```xml + + + + + + +``` + +### 部署配置 + +**操作**: 确保配置文件正确复制到输出目录 + +**验证**: 检查 `bin/Debug/net9.0/extend/` 目录包含所有必要文件 + +--- + +## 错误处理和恢复机制 + +### 全局异常处理 + +在所有拦截器中添加异常处理,确保: +1. 拦截器失败不影响主程序运行 +2. 错误信息被记录但不中断流程 +3. 可以通过配置禁用特定拦截器 + +### 日志恢复机制 + +在程序退出时确保: +1. Console输出被正确恢复 +2. 所有拦截器被正确清理 +3. 资源被正确释放 + +--- + +## 总结 + +本开发步骤文档提供了完整的插件管理器流接管机制实现方案,包括: + +1. **核心接口扩展** - 支持流拦截的新接口定义 +2. **输入流接管** - 命令行参数和选项的拦截处理 +3. **输出流接管** - 文件输出和路径重定向处理 +4. **日志流接管** - 完整的日志输出拦截和重定向 +5. **插件系统升级** - 支持流拦截器的插件管理 +6. **测试验证** - 确保功能正常工作的验证步骤 + +该方案严格遵循最小侵入原则,所有修改都在extend目录中进行,不影响原有核心代码的稳定性。通过配置文件可以灵活控制各种拦截功能,实现强大的插件扩展能力。 \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/UASwitcherPlugin.cs\350\257\267\346\261\202\345\244\264\345\210\207\346\215\242\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" "b/src/N_m3u8DL-RE/extend/extend-document/UASwitcherPlugin.cs\350\257\267\346\261\202\345\244\264\345\210\207\346\215\242\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" new file mode 100644 index 00000000..893ebb51 --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/UASwitcherPlugin.cs\350\257\267\346\261\202\345\244\264\345\210\207\346\215\242\346\217\222\344\273\266\345\274\200\345\217\221\346\226\207\346\241\243.md" @@ -0,0 +1,265 @@ +# UASwitcherPlugin.cs 请求头切换插件开发文档 + +## 插件概述 + +UASwitcherPlugin 是 N_m3u8DL-RE 插件系统中的一个核心组件,主要功能是在批量下载过程中自动切换 HTTP 请求头中的 User-Agent,实现请求头轮换和反反爬策略。 + +## 功能特性 + +### 1. User-Agent 轮换 +- **自动切换**: 每下载 1 个文件自动切换一次 User-Agent +- **循环使用**: 当所有 User-Agent 使用完后,重新从列表开头开始 +- **配置驱动**: 支持从 PluginConfig.json 读取自定义 User-Agent 列表 + +### 2. 默认 User-Agent 列表 +插件内置了三个主流浏览器的 User-Agent: +```csharp +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +"Mozilla/5.0 (Linux; Android 10; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36" +``` + +### 3. 插件接口实现 +实现了 IPlugin 接口的所有必需方法: +- `Initialize()`: 插件初始化和配置加载 +- `OnFileDownloaded()`: 文件下载完成回调 +- `OnInputReceived()`: 输入处理回调(预留接口) +- `OnOutputGenerated()`: 输出生成回调(预留接口) +- `OnLogGenerated()`: 日志生成回调(预留接口) + +## 调试环境设置 + +### 1. 必要工具 +- **mitmdump**: HTTP/HTTPS 代理服务器,用于拦截和分析网络请求 +- **ua_monitor.py**: 自定义的 User-Agent 监控脚本 +- **dotnet**: .NET 运行时,用于运行 N_m3u8DL-RE + +### 2. 调试环境架构 +``` +客户端应用 → mitmdump 代理 → 目标服务器 + ↓ ↓ ↓ + ua_monitor.py 拦截请求 响应数据 +``` + +### 3. 启动步骤 + +#### 步骤 1: 启动 UA 监听器 +```bash +mitmdump --listen-port 8083 --listen-host 127.0.0.1 -s /workspace/ua_monitor.py +``` + +#### 步骤 2: 启动批量下载(设置代理) +```bash +cd /workspace/N_m3u8DL-RE-src/src/N_m3u8DL-RE +export HTTP_PROXY=http://127.0.0.1:8083 +export HTTPS_PROXY=http://127.0.0.1:8083 +dotnet run -- --batch +``` + +## 源代码结构分析 + +### 核心类定义 +```csharp +public class UASwitcherPlugin : IPlugin +``` + +### 主要字段 +- `_config`: 插件配置对象 +- `_userAgents`: User-Agent 字符串列表 +- `_currentIndex`: 当前使用的 User-Agent 索引 + +### 核心方法 + +#### 1. Initialize() 方法 +```csharp +public void Initialize(PluginConfig? config) +``` +**功能**: +- 加载插件配置 +- 如果配置中有自定义 User-Agent,则替换默认列表 +- 记录初始化日志 + +**逻辑流程**: +1. 保存配置引用 +2. 检查配置中的 User-Agent 列表 +3. 如果有自定义配置,清空默认列表并添加自定义 UA +4. 记录初始化信息 + +#### 2. OnFileDownloaded() 方法 +```csharp +public void OnFileDownloaded(string filePath, int downloadCount) +``` +**功能**: +- 在每个文件下载完成后调用 +- 根据下载计数计算下一个要使用的 User-Agent +- 记录切换日志 + +**逻辑流程**: +1. 记录文件下载完成信息 +2. 计算 User-Agent 索引:`downloadCount % _userAgents.Count` +3. 获取新的 User-Agent +4. 记录切换信息 +5. 实际应用中需要修改全局 HTTP 客户端的默认请求头 + +## 使用方法 + +### 1. 配置文件设置 +在 `PluginConfig.json` 中启用和配置 UASwitcher 插件: + +```json +{ + "UASwitcher": { + "Enabled": true, + "UserAgents": [ + "自定义 User-Agent 1", + "自定义 User-Agent 2", + "自定义 User-Agent 3" + ] + } +} +``` + +### 2. 程序内调用 +插件通过 PluginManager 自动加载和调用: +```csharp +// 程序启动时 +pluginManager.InitializePlugins(config); + +// 文件下载完成后 +pluginManager.OnFileDownloaded(filePath, downloadCount); +``` + +### 3. 命令行使用 +```bash +# 启用批量模式 +dotnet run -- --batch + +# 设置代理(调试模式) +export HTTP_PROXY=http://127.0.0.1:8083 +export HTTPS_PROXY=http://127.0.0.1:8083 +``` + +## 调试指南 + +### 1. 验证插件加载 +检查程序启动日志,寻找类似以下信息: +``` +[UASwitcherPlugin] Initialized with headers: -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..." +``` + +### 2. 监控 User-Agent 切换 +通过 mitmdump 代理观察每次请求的 User-Agent 是否按预期切换: +```bash +# 监控代理日志 +mitmdump --listen-port 8083 -s ua_monitor.py +``` + +### 3. 验证切换逻辑 +观察程序日志,确认 User-Agent 按预期切换: +``` +[UASwitcherPlugin] File downloaded: /path/to/file.ts, count: 1 +[UASwitcherPlugin] Downloaded 1 files, switching UA to: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)... +``` + +### 4. 常见问题排查 + +#### 问题 1: 插件未生效 +**可能原因**: +- 插件未在配置中启用 +- 配置文件路径错误 + +**解决方案**: +- 检查 `PluginConfig.json` 中的 `Enabled` 字段 +- 确认配置文件被正确加载 + +#### 问题 2: User-Agent 未切换 +**可能原因**: +- HTTP 客户端未集成插件逻辑 +- 代理设置不正确 + +**解决方案**: +- 确认 HTTP 客户端使用插件返回的 User-Agent +- 检查代理设置和环境变量 + +#### 问题 3: 调试信息未显示 +**可能原因**: +- 日志级别设置过高 +- 日志输出重定向 + +**解决方案**: +- 检查日志配置 +- 确认控制台输出未被重定向 + +## 扩展开发 + +### 1. 添加新的回调方法 +插件预留了扩展接口,可以实现更多功能: +```csharp +public void OnInputReceived(object args, object option) +public void OnOutputGenerated(string outputPath, string outputType) +public void OnLogGenerated(string logMessage, PluginLogLevel logLevel) +``` + +### 2. 集成 HTTP 客户端 +实际使用中需要将 User-Agent 应用到 HTTP 客户端: +```csharp +// 示例:在 HTTP 客户端中应用 User-Agent +HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(newUA); +``` + +### 3. 添加请求头轮换 +可以扩展支持更多 HTTP 请求头的轮换: +```csharp +private readonly Dictionary> _headers = new Dictionary> +{ + { "User-Agent", userAgents }, + { "Accept-Language", new List { "en-US", "zh-CN", "ja-JP" } } +}; +``` + +## 性能考虑 + +### 1. 内存使用 +- User-Agent 列表存储在内存中 +- 默认列表较小(3个元素),内存占用可忽略 + +### 2. 性能影响 +- 计算 User-Agent 索引使用模运算,性能影响微乎其微 +- 日志记录可能对性能有一定影响,可根据需要调整日志级别 + +### 3. 扩展性 +- 支持任意数量的 User-Agent +- 可扩展支持其他请求头的轮换 + +## 最佳实践 + +### 1. 配置管理 +- 使用配置文件管理 User-Agent 列表 +- 根据目标网站特性选择合适的 User-Agent + +### 2. 调试建议 +- 在开发环境使用代理工具验证请求头 +- 记录详细的调试日志便于问题排查 + +### 3. 部署考虑 +- 生产环境中适当调整日志级别 +- 监控 User-Agent 切换效果 + +## 注意事项 + +1. **向后兼容**: 插件实现了向后兼容的接口方法 +2. **配置优先**: 自定义配置会覆盖默认 User-Agent 列表 +3. **线程安全**: 当前实现不是线程安全的,在多线程环境下需要额外的同步机制 +4. **错误处理**: 插件具有基础的错误处理机制,但可以进一步完善 +5. **代理依赖**: 调试功能依赖 mitmdump 代理,代理异常可能影响调试效果 + +## 版本信息 + +- **当前版本**: 1.0.0 +- **兼容性**: N_m3u8DL-RE 插件系统 +- **依赖项**: .NET 9.0+, System.Net.Http +- **最后更新**: 2025-12-16 + +--- + +本文档详细介绍了 UASwitcherPlugin 的功能、使用方法和调试过程。如有问题,请参考调试章节的故障排除部分。 \ No newline at end of file diff --git a/src/N_m3u8DL-RE/extend/extend-document/if-u-develop-extend-README.md b/src/N_m3u8DL-RE/extend/extend-document/if-u-develop-extend-README.md new file mode 100644 index 00000000..599d713a --- /dev/null +++ b/src/N_m3u8DL-RE/extend/extend-document/if-u-develop-extend-README.md @@ -0,0 +1,156 @@ +# N_m3u8DL-RE 插件系统开发文档 + +本文档介绍如何为 N_m3u8DL-RE 开发和使用插件系统。 + +## 1. 插件系统架构设计 + +插件系统具有以下特点: + +- **非侵入性**: 所有插件代码都放在 `extend` 目录中,不会影响主程序的核心代码 +- **可扩展性**: 通过 [IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89) 接口可以轻松添加新的插件功能 +- **配置驱动**: 插件行为可以通过 [PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json) 配置文件进行管理 +- **事件驱动**: 通过 `OnFileDownloaded` 事件触发插件逻辑 + +## 2. 核心组件 + +### PluginManager.cs (插件管理器) +- 实现了插件的加载、初始化和事件分发功能 +- 支持从配置文件中读取插件设置 +- 提供了统计下载次数的功能 + +### UASwitcherPlugin.cs (UA切换插件) +- 实现了每下载一定数量文件切换一次User-Agent的功能 +- 支持从配置文件中读取自定义User-Agent列表 + +### ProxySwitcherPlugin.cs (代理切换插件) +- 实现了每下载一定数量文件切换一次代理的功能 +- 通过Clash API控制代理切换 + +### BatchDownloadPlugin-and-input-output/BatchDownloadPlugin.cs (批量下载插件) +- 实现了批量下载多个URL的功能 +- 支持从配置文件读取URL列表 +- 自动生成包含原始URL信息的唯一文件名 +- 支持批量进度跟踪和错误处理 +- **配置架构优化**: 直接读取PluginConfig.json,无需中间配置类 + +### PluginConfig.json (配置文件) +- 控制各个插件的启用状态和行为参数 + +## 3. 插件接口规范 + +所有插件都需要实现 [IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89) 接口: + +```csharp +public interface IPlugin +{ + void Initialize(PluginConfig? config); + void OnFileDownloaded(string filePath, int downloadCount); +} +``` + +- `Initialize`: 插件初始化方法,在程序启动时调用 +- `OnFileDownloaded`: 文件下载完成回调,在每个文件下载完成后调用 + +## 4. 集成到主程序 + +插件系统已集成到主程序中: + +### SimpleDownloader.cs 集成 +- 在文件下载完成后添加了插件钩子调用 +- 确保无论下载成功还是跳过已存在的文件都会触发插件事件 + +### N_m3u8DL-RE.csproj 集成 +- 添加了对extend目录中插件文件的引用 +- 确保插件配置文件会被复制到输出目录 + +### Program.cs 集成 +- 在程序入口点初始化插件管理器 + +## 5. 设计优势 + +- **避免冲突**: 所有修改都在extend目录和必要的集成点,与原作者的开发路径完全分离 +- **易于维护**: 插件系统采用模块化设计,便于单独维护和升级 +- **高度可配置**: 通过配置文件可以灵活控制插件的行为 +- **易于扩展**: 可以通过实现[IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89)接口轻松添加新功能 + +## 6. 使用说明 + +要使用这个插件系统: + +1. 确保extend目录中的插件文件被正确编译 +2. 根据需要修改[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json)中的配置 +3. 程序运行时会自动加载启用的插件 +4. 每当一个文件下载完成时,会自动触发相应的插件逻辑 + +### 批量下载插件使用 + +要使用批量下载插件: + +1. 在[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json)中启用BatchDownload +2. 准备URL列表文件(默认路径:`extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt`) +3. 使用命令行参数 `--batch` 启用批量模式 +4. 指定输出目录:`--save-dir /path/to/output` + +**配置架构优化说明**: +- **统一配置源**: 所有配置直接从PluginConfig.json读取,无需中间配置类 +- **消除冲突**: 解决了配置冲突问题,确保单一配置来源 +- **灵活配置**: 支持直接在PluginConfig.json中修改所有参数 + +**命令示例**: +```bash +dotnet run -- --batch --save-dir /workspace/mpegts.js/demo/output +``` + +**输入文件格式**: +``` +# 注释行以#开头 +https://example1.com/video1.m3u8 +https://example2.com/video2.m3u8 +``` + +**输出文件命名**: +批量下载会自动生成包含原始URL信息的唯一文件名,格式为: +`{URL基础名}_batch{索引}_of_{总数}_{时间戳}.{扩展名}` + +## 7. 开发新插件 + +要开发一个新的插件,需要: + +1. 创建新的插件类,实现[IPlugin](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L85-L89)接口 +2. 在[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json)中添加插件配置项 +3. 在[PluginManager.cs](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs)的[LoadPlugins](file:///workspace/N_m3u8DL-RE-src/extend/PluginManager.cs#L16-L44)方法中添加插件加载逻辑 +4. 编译并测试插件功能 + +## 8. 配置文件说明 + +[PluginConfig.json](file:///workspace/N_m3u8DL-RE-src/extend/PluginConfig.json) 是插件系统的配置文件,支持以下配置项: + +```json +{ + "UASwitcher": { + "Enabled": true, + "UserAgents": [ + "UA1", + "UA2", + "UA3" + ] + }, + "ProxySwitcher": { + "Enabled": true, + "ClashApiUrl": "http://127.0.0.1:9090", + "SwitchInterval": 3 + }, + "BatchDownload": { + "Enabled": true, + "CreateSubdirectories": false, + "MaxConcurrency": 3 + } +} +``` + +- `Enabled`: 控制插件是否启用 +- `UserAgents`: UA切换插件使用的User-Agent列表 +- `ClashApiUrl`: 代理切换插件使用的Clash API地址 +- `SwitchInterval`: 切换间隔(每下载多少个文件切换一次) +- `CreateSubdirectories`: 批量下载插件是否为每个URL创建子目录 +- `MaxConcurrency`: 批量下载插件的最大并发数(预留功能) \ No newline at end of file diff --git "a/src/N_m3u8DL-RE/extend/extend-document/\345\274\200\345\217\221\345\217\230\346\233\264\346\200\273\347\273\223.md" "b/src/N_m3u8DL-RE/extend/extend-document/\345\274\200\345\217\221\345\217\230\346\233\264\346\200\273\347\273\223.md" new file mode 100644 index 00000000..91fe3fb8 --- /dev/null +++ "b/src/N_m3u8DL-RE/extend/extend-document/\345\274\200\345\217\221\345\217\230\346\233\264\346\200\273\347\273\223.md" @@ -0,0 +1,109 @@ +# BatchDownload 插件开发变更总结 + +## 最新配置架构优化 (重要变更) + +### 配置统一化重构 +- **删除文件**: `BatchDownloadConfig.cs` - 移除了冗余的配置类 +- **统一配置源**: 所有配置现在直接从 `PluginConfig.json` 读取 +- **输入文件统一**: 使用统一的输入文件路径 `extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt` +- **消除冲突**: 解决了配置冲突问题,确保单一配置来源 + +### 配置解析方法优化 +**在 BatchDownloadPlugin-and-input-output/BatchDownloadPlugin.cs 中新增直接JSON解析方法**: +- `private bool ExtractEnabledFromConfig()` - 提取启用状态 +- `private bool ExtractCreateSubdirectoriesFromConfig()` - 提取子目录创建配置 +- **优势**: 消除硬编码依赖,提高配置灵活性 + +**在 PluginManager.cs 中新增配置提取方法**: +- `private static bool ExtractBatchDownloadEnabledFromConfig()` +- **功能**: 为 Program.cs 提供统一的配置获取接口 + +### 插件检测逻辑优化 +**在 Program.cs 中优化插件检测逻辑**: +- **移除**: 旧的反射配置获取方式 (`config?.BatchDownload`) +- **新增**: 使用 `ExtractBatchDownloadEnabledFromConfig()` 方法 +- **修复**: 解决了"No URLs available"运行时错误 +- **改进**: 插件实例获取逻辑更加稳定可靠 + +## 在 Program.cs 中的修改 + +### 在 [Program.cs] [800行附近] 中新增了 `GetUniqueFileNameFromUrl` 方法 +**功能**: 根据URL和批量索引生成唯一文件名 +**参数**: string url, int batchIndex, int totalBatches +**返回值**: string 格式为 {URL基础名}_batch{索引}_of_{总数}_{时间戳} + +### 在 [Program.cs] [561行附近] 中新增了 `ExecuteBatchDownload` 方法 +**功能**: 执行批量下载逻辑 +**参数**: dynamic batchPlugin, MyOption option +**返回值**: async Task + +### 在 [Program.cs] [675行附近] 中修改了 `ExecuteSingleDownload` 函数 +**新增参数**: +- int batchIndex = 0: 批量索引 +- int totalBatches = 0: 总批量数 +- bool batchDownload = false: 是否为批量下载模式 + +### 在 [Program.cs] [624-626行附近] 中修改了批量下载循环逻辑 +**新增**: option.SaveName = null; 确保每个URL生成唯一文件名 + +### 在 [Program.cs] [261-344行附近] 中新增了批量下载插件检测逻辑 (已优化) +**优化内容**: 使用新的配置提取方法,提高检测可靠性 + +## 在 CommandLine/CommandInvoker.cs 中的修改 + +### 在 [CommandLine/CommandInvoker.cs] [34行附近] 中新增了 `BatchMode` 命令行选项 +**新增**: private static readonly Option BatchMode = new("--batch") + +### 在 [CommandLine/CommandInvoker.cs] [36行附近] 中修改了 `SaveDir` 选项 +**修改**: 参数名从 --dir 改为 --save-dir + +### 在 [CommandLine/CommandInvoker.cs] [617,632行附近] 中修改了 `GetOptions` 函数 +**新增参数**: +- BatchMode = result.GetValue(BatchMode) +- SaveDir = result.GetValue(SaveDir) + +### 在 [CommandLine/CommandInvoker.cs] [732行附近] 中修改了 `RootCommand` 配置 +**新增**: 将 BatchMode 和 SaveDir 添加到根命令选项中 + +## 在 CommandLine/MyOption.cs 中的修改 + +### 在 [CommandLine/MyOption.cs] [18行附近] 中新增了 `BatchMode` 属性 +**新增**: public bool BatchMode { get; set; } + +## 在 N_m3u8DL-RE.csproj 中的修改 + +**无修改** - 批量下载功能作为插件扩展添加,无需修改主项目配置 + +## 在 Downloader/SimpleDownloader.cs 中的修改 + +**无修改** - 批量下载功能主要涉及程序逻辑层,不需要修改下载器组件 + +## BatchDownloadPlugin 调用链 + +``` +Main() → 检测批量模式 → ExecuteBatchDownload() → 循环处理URL列表 → +ExecuteSingleDownload(url, option, batchIndex, totalBatches, batchDownload=true) → +GetUniqueFileNameFromUrl() → 生成唯一文件名 → 执行下载 +``` + +## 参数传递链 + +``` +命令行参数 --batch --save-dir → MyOption.BatchMode + MyOption.SaveDir → +ExecuteBatchDownload() → ExecuteSingleDownload() → +GetUniqueFileNameFromUrl() → option.SaveName → 文件写出 +``` + +## 批量下载使用方法 + +```bash +# 基本用法 +dotnet run -- --batch --save-dir /workspace/mpegts.js/demo/output + +# 带详细日志 +dotnet run -- --batch --save-dir /output --log-level debug +``` + +**输入文件**: `略/extend/BatchDownloadPlugin-and-input-output/input-batch-urls.txt` (每行一个URL) +**输出格式**: `{URL基础名}_batch{索引}_of_{总数}_{时间戳}.{扩展名}` +**示例文件名**: `7d7157190fe28708-9c54c7045ab91221e04441539478c65f-hls_720p_2_batch01_of_02_2025-12-15_03-27-14.mp4` \ No newline at end of file