Skip to content

Commit d29cd74

Browse files
committed
feat(bilibili-analyzer): migrate prepare script from Python to C#
- Replace Python prepare.py with C# prepare.cs using .NET 10 single-file execution - Update installation documentation to require .NET 10 SDK instead of Python - Migrate script execution from `python scripts/prepare.py` to `dotnet run scripts/prepare.cs` - Simplify yt-dlp installation instructions with direct executable download option - Update all usage examples and documentation references to reflect C# implementation - Bump version from 3.0.0 to 4.0.0 and buildId from 20260128.3 to 20260128.4 - Update skills/index.json to reference prepare.cs instead of prepare.py - Implement robust tool detection for yt-dlp and ffmpeg with helpful error messages - Maintain feature parity with original Python implementation including fps control and video-only/frames-only modes
1 parent 87ac296 commit d29cd74

4 files changed

Lines changed: 291 additions & 232 deletions

File tree

skills/index.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,11 +345,11 @@
345345
"basePath": "skills/tools/bilibili-analyzer",
346346
"files": [
347347
{ "path": "SKILL.md", "sha256": "PENDING" },
348-
{ "path": "scripts/prepare.py", "sha256": "PENDING" }
348+
{ "path": "scripts/prepare.cs", "sha256": "PENDING" }
349349
]
350350
},
351-
"version": "3.0.0",
352-
"buildId": "20260128.3",
351+
"version": "4.0.0",
352+
"buildId": "20260128.4",
353353
"status": "active",
354354
"updatedAt": "2026-01-28T00:00:00Z"
355355
}

skills/tools/bilibili-analyzer/SKILL.md

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,26 @@ B站视频内容分析工具。提供视频URL后,自动下载视频、拆解
3636

3737
## Installation
3838

39-
### 1. 安装 yt-dlp
39+
### 1. 安装 .NET 10 SDK
40+
41+
脚本使用 .NET 10 单文件执行功能,需要安装 .NET 10 SDK。
42+
43+
下载地址: https://dotnet.microsoft.com/download/dotnet/10.0
44+
45+
验证安装:
46+
```bash
47+
dotnet --version
48+
```
49+
50+
### 2. 安装 yt-dlp
4051

4152
```bash
4253
pip install yt-dlp
4354
```
4455

45-
详细安装说明: https://github.com/yt-dlp/yt-dlp#installation
56+
或直接下载可执行文件: https://github.com/yt-dlp/yt-dlp/releases
4657

47-
### 2. 安装 FFmpeg
58+
### 3. 安装 FFmpeg
4859

4960
**Windows:**
5061
```powershell
@@ -85,21 +96,23 @@ ffmpeg -version
8596

8697
## Provided Script
8798

88-
本 skill 提供了 `scripts/prepare.py` 脚本用于下载视频和提取帧图片。
99+
本 skill 提供了 `scripts/prepare.cs` 脚本用于下载视频和提取帧图片。
100+
101+
**脚本位置**: `skills/tools/bilibili-analyzer/scripts/prepare.cs`
89102

90-
**脚本位置**: `skills/tools/bilibili-analyzer/scripts/prepare.py`
103+
**运行方式**: 使用 .NET 10 单文件执行功能
91104

92105
### 使用方法
93106

94107
```bash
95108
# 基本用法
96-
python scripts/prepare.py "<视频URL>" -o <输出目录>
109+
dotnet run scripts/prepare.cs "<视频URL>" -o <输出目录>
97110

98111
# 示例
99-
python scripts/prepare.py "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output
112+
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output
100113

101114
# 长视频(降低帧率)
102-
python scripts/prepare.py "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output --fps 0.5
115+
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output --fps 0.5
103116
```
104117

105118
### 参数说明
@@ -133,7 +146,7 @@ python scripts/prepare.py "https://www.bilibili.com/video/BV1xx411c7mD" -o ./out
133146
使用提供的脚本下载视频并拆解成帧图片:
134147

135148
```bash
136-
python skills/tools/bilibili-analyzer/scripts/prepare.py "<视频URL>" -o <输出目录>
149+
dotnet run skills/tools/bilibili-analyzer/scripts/prepare.cs "<视频URL>" -o <输出目录>
137150
```
138151

139152
**注意事项**:
@@ -401,7 +414,7 @@ ffmpeg -i video.mp4 -vf "select='gt(scene,0.3)'" -vsync vfr -q:v 2 images/frame_
401414

402415
```bash
403416
# 1. 下载并拆帧
404-
python scripts/prepare.py "https://www.bilibili.com/video/BV1xx411c7mD" -o ./react-tutorial
417+
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./react-tutorial
405418

406419
# 2. 分析图片(使用 Task 工具)
407420
# 3. 生成 react-tutorial/视频分析.md
@@ -411,13 +424,13 @@ python scripts/prepare.py "https://www.bilibili.com/video/BV1xx411c7mD" -o ./rea
411424

412425
```bash
413426
# 降低帧率,减少图片数量
414-
python scripts/prepare.py "https://www.bilibili.com/video/BV1xx411c7mD" -o ./long-video --fps 0.2
427+
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./long-video --fps 0.2
415428
```
416429

417430
### 示例3: 只下载视频
418431

419432
```bash
420-
python scripts/prepare.py "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output --video-only
433+
dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o ./output --video-only
421434
```
422435

423436
## Quality Checklist
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
#!/usr/bin/env dotnet run
2+
#:package CliWrap@3.6.6
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using CliWrap;
10+
using CliWrap.Buffered;
11+
12+
// Parse arguments
13+
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
14+
if (args.Length == 0 || args.Contains("-h") || args.Contains("--help"))
15+
{
16+
PrintHelp();
17+
return;
18+
}
19+
20+
var url = args[0];
21+
var outputDir = GetArgValue(args, "-o", "--output") ?? ".";
22+
var fps = double.Parse(GetArgValue(args, "--fps") ?? "1.0");
23+
var videoOnly = args.Contains("--video-only");
24+
var framesOnly = args.Contains("--frames-only");
25+
26+
var videoPath = Path.Combine(outputDir, "video.mp4");
27+
var imagesDir = Path.Combine(outputDir, "images");
28+
29+
// Create output directory
30+
Directory.CreateDirectory(outputDir);
31+
32+
Console.WriteLine(new string('=', 50));
33+
Console.WriteLine("Bilibili Video Analyzer - Prepare Script");
34+
Console.WriteLine(new string('=', 50));
35+
Console.WriteLine($"URL: {url}");
36+
Console.WriteLine($"Output: {outputDir}");
37+
Console.WriteLine($"FPS: {fps}");
38+
Console.WriteLine(new string('=', 50));
39+
40+
// Download video
41+
if (!framesOnly)
42+
{
43+
if (!await DownloadVideoAsync(url, videoPath))
44+
{
45+
Environment.Exit(1);
46+
}
47+
}
48+
49+
// Extract frames
50+
if (!videoOnly)
51+
{
52+
if (!File.Exists(videoPath))
53+
{
54+
Console.WriteLine($"[ERROR] Video file not found: {videoPath}");
55+
Environment.Exit(1);
56+
}
57+
58+
if (!await ExtractFramesAsync(videoPath, imagesDir, fps))
59+
{
60+
Environment.Exit(1);
61+
}
62+
}
63+
64+
Console.WriteLine();
65+
Console.WriteLine(new string('=', 50));
66+
Console.WriteLine("[OK] Done!");
67+
Console.WriteLine($" Video: {videoPath}");
68+
Console.WriteLine($" Images: {imagesDir}/");
69+
Console.WriteLine(new string('=', 50));
70+
71+
// === Functions ===
72+
73+
async Task<bool> DownloadVideoAsync(string url, string outputPath)
74+
{
75+
Console.WriteLine($"[INFO] Downloading video: {url}");
76+
77+
var ytDlp = FindExecutable("yt-dlp", "yt-dlp.exe");
78+
if (ytDlp == null)
79+
{
80+
Console.WriteLine("[ERROR] yt-dlp not found!");
81+
Console.WriteLine(" Install with: pip install yt-dlp");
82+
Console.WriteLine(" Or download from: https://github.com/yt-dlp/yt-dlp/releases");
83+
return false;
84+
}
85+
86+
try
87+
{
88+
Console.WriteLine($"[INFO] Running yt-dlp...");
89+
var result = await Cli.Wrap(ytDlp)
90+
.WithArguments(new[]
91+
{
92+
"-f", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
93+
"-o", outputPath,
94+
"--no-warnings",
95+
url
96+
})
97+
.WithValidation(CommandResultValidation.None)
98+
.ExecuteBufferedAsync();
99+
100+
if (result.ExitCode != 0)
101+
{
102+
Console.WriteLine($"[ERROR] Download failed: {result.StandardError}");
103+
return false;
104+
}
105+
106+
Console.WriteLine($"[OK] Video downloaded: {outputPath}");
107+
return true;
108+
}
109+
catch (Exception ex)
110+
{
111+
Console.WriteLine($"[ERROR] Download failed: {ex.Message}");
112+
return false;
113+
}
114+
}
115+
116+
async Task<bool> ExtractFramesAsync(string videoPath, string outputDir, double fps)
117+
{
118+
Console.WriteLine($"[INFO] Extracting frames (fps={fps})");
119+
120+
var ffmpeg = FindExecutable("ffmpeg", "ffmpeg.exe");
121+
if (ffmpeg == null)
122+
{
123+
Console.WriteLine("[ERROR] ffmpeg not found!");
124+
Console.WriteLine(" Windows: choco install ffmpeg / scoop install ffmpeg");
125+
Console.WriteLine(" macOS: brew install ffmpeg");
126+
Console.WriteLine(" Linux: sudo apt install ffmpeg");
127+
return false;
128+
}
129+
130+
Directory.CreateDirectory(outputDir);
131+
var outputPattern = Path.Combine(outputDir, "frame_%04d.jpg");
132+
133+
try
134+
{
135+
Console.WriteLine($"[INFO] Running ffmpeg...");
136+
var result = await Cli.Wrap(ffmpeg)
137+
.WithArguments(new[]
138+
{
139+
"-i", videoPath,
140+
"-vf", $"fps={fps}",
141+
"-q:v", "2",
142+
"-y",
143+
outputPattern
144+
})
145+
.WithValidation(CommandResultValidation.None)
146+
.ExecuteBufferedAsync();
147+
148+
if (result.ExitCode != 0)
149+
{
150+
Console.WriteLine($"[ERROR] Frame extraction failed: {result.StandardError}");
151+
return false;
152+
}
153+
154+
var frameCount = Directory.GetFiles(outputDir, "frame_*.jpg").Length;
155+
Console.WriteLine($"[OK] Frames extracted: {frameCount} images saved to {outputDir}/");
156+
return true;
157+
}
158+
catch (Exception ex)
159+
{
160+
Console.WriteLine($"[ERROR] Frame extraction failed: {ex.Message}");
161+
return false;
162+
}
163+
}
164+
165+
string? FindExecutable(params string[] names)
166+
{
167+
// Check PATH
168+
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
169+
var paths = pathEnv.Split(Path.PathSeparator);
170+
171+
foreach (var name in names)
172+
{
173+
// Direct check
174+
foreach (var path in paths)
175+
{
176+
var fullPath = Path.Combine(path, name);
177+
if (File.Exists(fullPath)) return fullPath;
178+
}
179+
}
180+
181+
// Common Windows paths
182+
var commonPaths = new[]
183+
{
184+
@"C:\ffmpeg\bin",
185+
@"C:\Program Files\ffmpeg\bin",
186+
@"C:\tools\ffmpeg\bin",
187+
Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Microsoft\WinGet\Packages"),
188+
Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\scoop\shims"),
189+
};
190+
191+
foreach (var basePath in commonPaths)
192+
{
193+
foreach (var name in names)
194+
{
195+
var fullPath = Path.Combine(basePath, name);
196+
if (File.Exists(fullPath)) return fullPath;
197+
}
198+
}
199+
200+
// Try which/where command
201+
try
202+
{
203+
var cmd = OperatingSystem.IsWindows() ? "where" : "which";
204+
var result = Process.Start(new ProcessStartInfo
205+
{
206+
FileName = cmd,
207+
Arguments = names[0],
208+
RedirectStandardOutput = true,
209+
UseShellExecute = false,
210+
CreateNoWindow = true
211+
});
212+
result?.WaitForExit();
213+
var output = result?.StandardOutput.ReadToEnd()?.Trim();
214+
if (!string.IsNullOrEmpty(output) && File.Exists(output.Split('\n')[0]))
215+
{
216+
return output.Split('\n')[0].Trim();
217+
}
218+
}
219+
catch { }
220+
221+
return null;
222+
}
223+
224+
string? GetArgValue(string[] args, params string[] names)
225+
{
226+
for (int i = 0; i < args.Length - 1; i++)
227+
{
228+
if (names.Contains(args[i]))
229+
{
230+
return args[i + 1];
231+
}
232+
}
233+
return null;
234+
}
235+
236+
void PrintHelp()
237+
{
238+
Console.WriteLine(@"
239+
Bilibili Video Analyzer - Prepare Script
240+
241+
Usage:
242+
dotnet run prepare.cs <url> [options]
243+
244+
Arguments:
245+
url Bilibili video URL (required)
246+
247+
Options:
248+
-o, --output <dir> Output directory (default: current)
249+
--fps <value> Frames per second (default: 1.0)
250+
--video-only Only download video, skip frame extraction
251+
--frames-only Only extract frames (requires existing video.mp4)
252+
-h, --help Show this help
253+
254+
Examples:
255+
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD""
256+
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD"" --fps 0.5
257+
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD"" -o ./output
258+
259+
Requirements:
260+
- yt-dlp: pip install yt-dlp
261+
- ffmpeg: https://ffmpeg.org/download.html
262+
");
263+
}

0 commit comments

Comments
 (0)