|
| 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