Skip to content

Commit 5eef39a

Browse files
committed
feat(bilibili-analyzer): replace yt-dlp with native Bilibili API integration
- Remove yt-dlp dependency and replace with direct Bilibili API calls using HttpClient - Implement BV ID extraction from Bilibili URLs with regex validation - Add video info fetching to obtain CID and title from Bilibili API - Implement playback URL retrieval with quality selection (qn=80) - Add streaming video download with progress tracking and proper HTTP headers - Remove CliWrap package dependency for simpler, self-contained implementation - Update documentation to reflect Bilibili API as primary video source - Update installation instructions to remove yt-dlp setup steps - Update API reference section with Bilibili API endpoints and examples - Update tags from `yt-dlp` to `dotnet` to reflect new implementation - Simplify dependency management by using only .NET standard libraries
1 parent d29cd74 commit 5eef39a

2 files changed

Lines changed: 170 additions & 82 deletions

File tree

skills/tools/bilibili-analyzer/SKILL.md

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@ description: 自动分析B站视频内容,下载视频并拆解成帧图片,
44
metadata:
55
short-description: B站视频AI分析工具
66
source:
7-
- name: yt-dlp
8-
repository: https://github.com/yt-dlp/yt-dlp
9-
documentation: https://github.com/yt-dlp/yt-dlp#readme
10-
license: Unlicense
11-
stars: 90k+
127
- name: FFmpeg
138
repository: https://github.com/FFmpeg/FFmpeg
149
documentation: https://ffmpeg.org/documentation.html
1510
license: LGPL/GPL
11+
- name: Bilibili API
12+
documentation: https://github.com/SocialSisterYi/bilibili-API-collect
1613
---
1714

1815
# Bilibili Video Analyzer
@@ -31,8 +28,8 @@ B站视频内容分析工具。提供视频URL后,自动下载视频、拆解
3128

3229
| 工具 | 用途 | 文档 |
3330
|------|------|------|
34-
| **yt-dlp** | 视频下载 | [GitHub](https://github.com/yt-dlp/yt-dlp) / [Options](https://github.com/yt-dlp/yt-dlp#usage-and-options) |
3531
| **FFmpeg** | 视频拆帧 | [官网](https://ffmpeg.org/) / [文档](https://ffmpeg.org/ffmpeg.html) |
32+
| **Bilibili API** | 视频下载 | [API文档](https://github.com/SocialSisterYi/bilibili-API-collect) |
3633

3734
## Installation
3835

@@ -47,15 +44,7 @@ B站视频内容分析工具。提供视频URL后,自动下载视频、拆解
4744
dotnet --version
4845
```
4946

50-
### 2. 安装 yt-dlp
51-
52-
```bash
53-
pip install yt-dlp
54-
```
55-
56-
或直接下载可执行文件: https://github.com/yt-dlp/yt-dlp/releases
57-
58-
### 3. 安装 FFmpeg
47+
### 2. 安装 FFmpeg
5948

6049
**Windows:**
6150
```powershell
@@ -84,7 +73,6 @@ sudo yum install ffmpeg
8473

8574
验证安装:
8675
```bash
87-
yt-dlp --version
8876
ffmpeg -version
8977
```
9078

@@ -372,23 +360,19 @@ Task 3: 分析 frame_0041.jpg ~ frame_0060.jpg
372360

373361
## API Reference
374362

375-
### yt-dlp 常用命令
376-
377-
```bash
378-
# 下载最佳质量视频
379-
yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" -o video.mp4 "<URL>"
363+
### Bilibili API
380364

381-
# 只下载音频
382-
yt-dlp -x --audio-format mp3 -o audio.mp3 "<URL>"
365+
脚本使用 Bilibili 官方 API 下载视频:
383366

384-
# 查看可用格式
385-
yt-dlp -F "<URL>"
367+
```
368+
# 获取视频信息
369+
GET https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD
386370
387-
# 下载字幕
388-
yt-dlp --write-subs --sub-lang zh-Hans -o video.mp4 "<URL>"
371+
# 获取播放地址
372+
GET https://api.bilibili.com/x/player/playurl?bvid=BV1xx411c7mD&cid={cid}&qn=80&fnval=1
389373
```
390374

391-
更多选项: https://github.com/yt-dlp/yt-dlp#usage-and-options
375+
API 文档: https://github.com/SocialSisterYi/bilibili-API-collect
392376

393377
### FFmpeg 拆帧命令
394378

@@ -458,7 +442,7 @@ dotnet run scripts/prepare.cs "https://www.bilibili.com/video/BV1xx411c7mD" -o .
458442

459443
## Tags
460444

461-
`bilibili`, `video-analysis`, `ai`, `frame-extraction`, `markdown`, `tutorial`, `yt-dlp`, `ffmpeg`
445+
`bilibili`, `video-analysis`, `ai`, `frame-extraction`, `markdown`, `tutorial`, `ffmpeg`, `dotnet`
462446

463447
## Compatibility
464448

skills/tools/bilibili-analyzer/scripts/prepare.cs

Lines changed: 157 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/usr/bin/env dotnet run
2-
#:package CliWrap@3.6.6
32

43
using System;
54
using System.Diagnostics;
65
using System.IO;
76
using System.Linq;
7+
using System.Net.Http;
8+
using System.Text.Json;
9+
using System.Text.RegularExpressions;
810
using System.Threading.Tasks;
9-
using CliWrap;
10-
using CliWrap.Buffered;
1111

1212
// Parse arguments
1313
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
@@ -40,7 +40,7 @@
4040
// Download video
4141
if (!framesOnly)
4242
{
43-
if (!await DownloadVideoAsync(url, videoPath))
43+
if (!await DownloadBilibiliVideoAsync(url, videoPath))
4444
{
4545
Environment.Exit(1);
4646
}
@@ -70,39 +70,96 @@
7070

7171
// === Functions ===
7272

73-
async Task<bool> DownloadVideoAsync(string url, string outputPath)
73+
async Task<bool> DownloadBilibiliVideoAsync(string url, string outputPath)
7474
{
7575
Console.WriteLine($"[INFO] Downloading video: {url}");
7676

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-
8677
try
8778
{
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)
79+
// Extract BV ID from URL
80+
var bvid = ExtractBvid(url);
81+
if (string.IsNullOrEmpty(bvid))
82+
{
83+
Console.WriteLine("[ERROR] Invalid Bilibili URL, cannot extract BV ID");
84+
return false;
85+
}
86+
Console.WriteLine($"[INFO] BV ID: {bvid}");
87+
88+
using var client = CreateHttpClient();
89+
90+
// Step 1: Get video info to obtain cid
91+
Console.WriteLine("[INFO] Fetching video info...");
92+
var infoUrl = $"https://api.bilibili.com/x/web-interface/view?bvid={bvid}";
93+
var infoJson = await client.GetStringAsync(infoUrl);
94+
using var infoDoc = JsonDocument.Parse(infoJson);
95+
96+
var code = infoDoc.RootElement.GetProperty("code").GetInt32();
97+
if (code != 0)
10198
{
102-
Console.WriteLine($"[ERROR] Download failed: {result.StandardError}");
99+
var message = infoDoc.RootElement.GetProperty("message").GetString();
100+
Console.WriteLine($"[ERROR] Failed to get video info: {message}");
103101
return false;
104102
}
105103

104+
var data = infoDoc.RootElement.GetProperty("data");
105+
var title = data.GetProperty("title").GetString();
106+
var cid = data.GetProperty("cid").GetInt64();
107+
Console.WriteLine($"[INFO] Title: {title}");
108+
Console.WriteLine($"[INFO] CID: {cid}");
109+
110+
// Step 2: Get playback URL
111+
Console.WriteLine("[INFO] Fetching playback URL...");
112+
var playUrl = $"https://api.bilibili.com/x/player/playurl?bvid={bvid}&cid={cid}&qn=80&fnval=1";
113+
var playJson = await client.GetStringAsync(playUrl);
114+
using var playDoc = JsonDocument.Parse(playJson);
115+
116+
var playCode = playDoc.RootElement.GetProperty("code").GetInt32();
117+
if (playCode != 0)
118+
{
119+
var message = playDoc.RootElement.GetProperty("message").GetString();
120+
Console.WriteLine($"[ERROR] Failed to get playback URL: {message}");
121+
return false;
122+
}
123+
124+
var playData = playDoc.RootElement.GetProperty("data");
125+
var durl = playData.GetProperty("durl")[0];
126+
var videoUrl = durl.GetProperty("url").GetString();
127+
var size = durl.GetProperty("size").GetInt64();
128+
129+
Console.WriteLine($"[INFO] Video size: {size / 1024 / 1024:F1} MB");
130+
131+
// Step 3: Download video
132+
Console.WriteLine("[INFO] Downloading video file...");
133+
134+
using var request = new HttpRequestMessage(HttpMethod.Get, videoUrl);
135+
request.Headers.Add("Referer", $"https://www.bilibili.com/video/{bvid}");
136+
137+
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
138+
response.EnsureSuccessStatusCode();
139+
140+
var totalBytes = response.Content.Headers.ContentLength ?? size;
141+
142+
await using var contentStream = await response.Content.ReadAsStreamAsync();
143+
await using var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true);
144+
145+
var buffer = new byte[8192];
146+
var totalRead = 0L;
147+
var lastProgress = 0;
148+
int bytesRead;
149+
150+
while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
151+
{
152+
await fileStream.WriteAsync(buffer, 0, bytesRead);
153+
totalRead += bytesRead;
154+
155+
var progress = (int)(totalRead * 100 / totalBytes);
156+
if (progress > lastProgress && progress % 10 == 0)
157+
{
158+
Console.WriteLine($"[INFO] Progress: {progress}%");
159+
lastProgress = progress;
160+
}
161+
}
162+
106163
Console.WriteLine($"[OK] Video downloaded: {outputPath}");
107164
return true;
108165
}
@@ -113,6 +170,45 @@ async Task<bool> DownloadVideoAsync(string url, string outputPath)
113170
}
114171
}
115172

173+
string? ExtractBvid(string url)
174+
{
175+
// Match BV ID from various URL formats
176+
// https://www.bilibili.com/video/BV1xx411c7mD
177+
// https://b23.tv/BV1xx411c7mD
178+
// BV1xx411c7mD
179+
var patterns = new[]
180+
{
181+
@"BV[a-zA-Z0-9]+",
182+
};
183+
184+
foreach (var pattern in patterns)
185+
{
186+
var match = Regex.Match(url, pattern);
187+
if (match.Success)
188+
{
189+
return match.Value;
190+
}
191+
}
192+
193+
return null;
194+
}
195+
196+
HttpClient CreateHttpClient()
197+
{
198+
var handler = new HttpClientHandler
199+
{
200+
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
201+
};
202+
203+
var client = new HttpClient(handler);
204+
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
205+
client.DefaultRequestHeaders.Add("Referer", "https://www.bilibili.com");
206+
client.DefaultRequestHeaders.Add("Accept", "application/json, text/plain, */*");
207+
client.Timeout = TimeSpan.FromMinutes(30);
208+
209+
return client;
210+
}
211+
116212
async Task<bool> ExtractFramesAsync(string videoPath, string outputDir, double fps)
117213
{
118214
Console.WriteLine($"[INFO] Extracting frames (fps={fps})");
@@ -133,21 +229,30 @@ async Task<bool> ExtractFramesAsync(string videoPath, string outputDir, double f
133229
try
134230
{
135231
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)
232+
233+
var psi = new ProcessStartInfo
234+
{
235+
FileName = ffmpeg,
236+
Arguments = $"-i \"{videoPath}\" -vf \"fps={fps}\" -q:v 2 -y \"{outputPattern}\"",
237+
RedirectStandardOutput = true,
238+
RedirectStandardError = true,
239+
UseShellExecute = false,
240+
CreateNoWindow = true
241+
};
242+
243+
using var process = Process.Start(psi);
244+
if (process == null)
245+
{
246+
Console.WriteLine("[ERROR] Failed to start ffmpeg");
247+
return false;
248+
}
249+
250+
var stderr = await process.StandardError.ReadToEndAsync();
251+
await process.WaitForExitAsync();
252+
253+
if (process.ExitCode != 0)
149254
{
150-
Console.WriteLine($"[ERROR] Frame extraction failed: {result.StandardError}");
255+
Console.WriteLine($"[ERROR] Frame extraction failed: {stderr}");
151256
return false;
152257
}
153258

@@ -164,13 +269,11 @@ async Task<bool> ExtractFramesAsync(string videoPath, string outputDir, double f
164269

165270
string? FindExecutable(params string[] names)
166271
{
167-
// Check PATH
168272
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
169273
var paths = pathEnv.Split(Path.PathSeparator);
170274

171275
foreach (var name in names)
172276
{
173-
// Direct check
174277
foreach (var path in paths)
175278
{
176279
var fullPath = Path.Combine(path, name);
@@ -184,7 +287,6 @@ async Task<bool> ExtractFramesAsync(string videoPath, string outputDir, double f
184287
@"C:\ffmpeg\bin",
185288
@"C:\Program Files\ffmpeg\bin",
186289
@"C:\tools\ffmpeg\bin",
187-
Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Microsoft\WinGet\Packages"),
188290
Environment.ExpandEnvironmentVariables(@"%USERPROFILE%\scoop\shims"),
189291
};
190292

@@ -197,23 +299,25 @@ async Task<bool> ExtractFramesAsync(string videoPath, string outputDir, double f
197299
}
198300
}
199301

200-
// Try which/where command
302+
// Try where command on Windows
201303
try
202304
{
203305
var cmd = OperatingSystem.IsWindows() ? "where" : "which";
204-
var result = Process.Start(new ProcessStartInfo
306+
var psi = new ProcessStartInfo
205307
{
206308
FileName = cmd,
207309
Arguments = names[0],
208310
RedirectStandardOutput = true,
209311
UseShellExecute = false,
210312
CreateNoWindow = true
211-
});
212-
result?.WaitForExit();
213-
var output = result?.StandardOutput.ReadToEnd()?.Trim();
214-
if (!string.IsNullOrEmpty(output) && File.Exists(output.Split('\n')[0]))
313+
};
314+
using var process = Process.Start(psi);
315+
process?.WaitForExit();
316+
var output = process?.StandardOutput.ReadToEnd()?.Trim();
317+
if (!string.IsNullOrEmpty(output))
215318
{
216-
return output.Split('\n')[0].Trim();
319+
var firstLine = output.Split('\n')[0].Trim();
320+
if (File.Exists(firstLine)) return firstLine;
217321
}
218322
}
219323
catch { }
@@ -257,7 +361,7 @@ url Bilibili video URL (required)
257361
dotnet run prepare.cs ""https://www.bilibili.com/video/BV1xx411c7mD"" -o ./output
258362
259363
Requirements:
260-
- yt-dlp: pip install yt-dlp
364+
- .NET 10 SDK
261365
- ffmpeg: https://ffmpeg.org/download.html
262366
");
263367
}

0 commit comments

Comments
 (0)