├── .dockerignore ├── .editorconfig ├── .github ├── issue_template.md └── workflows │ └── build_latest.yml ├── .gitignore ├── BBDown.Core ├── APP │ ├── Header │ │ ├── device.proto │ │ ├── fawkesreq.proto │ │ ├── locale.proto │ │ ├── metadata.proto │ │ ├── network.proto │ │ └── restriction.proto │ ├── Payload │ │ ├── dmviewreq.proto │ │ └── playviewreq.proto │ └── Response │ │ ├── dmviewreply.proto │ │ └── playviewreply.proto ├── AppHelper.cs ├── BBDown.Core.csproj ├── Config.cs ├── DanmakuUtil.cs ├── Entity │ ├── Entity.cs │ ├── ParsedResult.cs │ └── VInfo.cs ├── Fetcher │ ├── BangumiInfoFetcher.cs │ ├── CheeseInfoFetcher.cs │ ├── FavListFetcher.cs │ ├── IntlBangumiInfoFetcher.cs │ ├── MediaListFetcher.cs │ ├── NormalInfoFetcher.cs │ ├── SeriesListFetcher.cs │ └── SpaceVideoFetcher.cs ├── FetcherFactory.cs ├── IFetcher.cs ├── Logger.cs ├── Parser.cs └── Util │ ├── BilibiliBvConverter.cs │ ├── HTTPUtil.cs │ └── SubUtil.cs ├── BBDown.sln ├── BBDown ├── BBDown.csproj ├── BBDownApiServer.cs ├── BBDownAria2c.cs ├── BBDownConfigParser.cs ├── BBDownDownloadUtil.cs ├── BBDownEnums.cs ├── BBDownLoginUtil.cs ├── BBDownMuxer.cs ├── BBDownUtil.cs ├── CommandLineInvoker.cs ├── ConsoleQRCode.cs ├── Directory.Build.props ├── Model │ └── ServeRequestOptions.cs ├── MyOption.cs ├── Program.Methods.cs ├── Program.cs ├── ProgressBar.cs └── Properties │ └── launchSettings.json ├── Dockerfile ├── LICENSE ├── README.md └── json-api-doc.md /.dockerignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Rider 20 | .idea 21 | 22 | # macOS shit 23 | .DS_Store 24 | 25 | # Build results 26 | [Dd]ebug/ 27 | [Dd]ebugPublic/ 28 | [Rr]elease/ 29 | [Rr]eleases/ 30 | x64/ 31 | x86/ 32 | [Ww][Ii][Nn]32/ 33 | [Aa][Rr][Mm]/ 34 | [Aa][Rr][Mm]64/ 35 | bld/ 36 | [Bb]in/ 37 | [Oo]bj/ 38 | [Ll]og/ 39 | [Ll]ogs/ 40 | 41 | # Visual Studio 2015/2017 cache/options directory 42 | .vs/ 43 | # Uncomment if you have tasks that create the project's static files in wwwroot 44 | #wwwroot/ 45 | 46 | # Visual Studio 2017 auto generated files 47 | Generated\ Files/ 48 | 49 | # MSTest test Results 50 | [Tt]est[Rr]esult*/ 51 | [Bb]uild[Ll]og.* 52 | 53 | # NUnit 54 | *.VisualState.xml 55 | TestResult.xml 56 | nunit-*.xml 57 | 58 | # Build Results of an ATL Project 59 | [Dd]ebugPS/ 60 | [Rr]eleasePS/ 61 | dlldata.c 62 | 63 | # Benchmark Results 64 | BenchmarkDotNet.Artifacts/ 65 | 66 | # .NET Core 67 | project.lock.json 68 | project.fragment.lock.json 69 | artifacts/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.vspscc 100 | *.vssscc 101 | .builds 102 | *.pidb 103 | *.svclog 104 | *.scc 105 | 106 | # Chutzpah Test files 107 | _Chutzpah* 108 | 109 | # Visual C++ cache files 110 | ipch/ 111 | *.aps 112 | *.ncb 113 | *.opendb 114 | *.opensdf 115 | *.sdf 116 | *.cachefile 117 | *.VC.db 118 | *.VC.VC.opendb 119 | 120 | # Visual Studio profiler 121 | *.psess 122 | *.vsp 123 | *.vspx 124 | *.sap 125 | 126 | # Visual Studio Trace Files 127 | *.e2e 128 | 129 | # TFS 2012 Local Workspace 130 | $tf/ 131 | 132 | # Guidance Automation Toolkit 133 | *.gpState 134 | 135 | # ReSharper is a .NET coding add-in 136 | _ReSharper*/ 137 | *.[Rr]e[Ss]harper 138 | *.DotSettings.user 139 | 140 | # TeamCity is a build add-in 141 | _TeamCity* 142 | 143 | # DotCover is a Code Coverage Tool 144 | *.dotCover 145 | 146 | # AxoCover is a Code Coverage Tool 147 | .axoCover/* 148 | !.axoCover/settings.json 149 | 150 | # Coverlet is a free, cross platform Code Coverage Tool 151 | coverage*.json 152 | coverage*.xml 153 | coverage*.info 154 | 155 | # Visual Studio code coverage results 156 | *.coverage 157 | *.coveragexml 158 | 159 | # NCrunch 160 | _NCrunch_* 161 | .*crunch*.local.xml 162 | nCrunchTemp_* 163 | 164 | # MightyMoose 165 | *.mm.* 166 | AutoTest.Net/ 167 | 168 | # Web workbench (sass) 169 | .sass-cache/ 170 | 171 | # Installshield output folder 172 | [Ee]xpress/ 173 | 174 | # DocProject is a documentation generator add-in 175 | DocProject/buildhelp/ 176 | DocProject/Help/*.HxT 177 | DocProject/Help/*.HxC 178 | DocProject/Help/*.hhc 179 | DocProject/Help/*.hhk 180 | DocProject/Help/*.hhp 181 | DocProject/Help/Html2 182 | DocProject/Help/html 183 | 184 | # Click-Once directory 185 | publish/ 186 | 187 | # Publish Web Output 188 | *.[Pp]ublish.xml 189 | *.azurePubxml 190 | # Note: Comment the next line if you want to checkin your web deploy settings, 191 | # but database connection strings (with potential passwords) will be unencrypted 192 | *.pubxml 193 | *.publishproj 194 | 195 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 196 | # checkin your Azure Web App publish settings, but sensitive information contained 197 | # in these scripts will be unencrypted 198 | PublishScripts/ 199 | 200 | # NuGet Packages 201 | *.nupkg 202 | # NuGet Symbol Packages 203 | *.snupkg 204 | # The packages folder can be ignored because of Package Restore 205 | **/[Pp]ackages/* 206 | # except build/, which is used as an MSBuild target. 207 | !**/[Pp]ackages/build/ 208 | # Uncomment if necessary however generally it will be regenerated when needed 209 | #!**/[Pp]ackages/repositories.config 210 | # NuGet v3's project.json files produces more ignorable files 211 | *.nuget.props 212 | *.nuget.targets 213 | 214 | # Microsoft Azure Build Output 215 | csx/ 216 | *.build.csdef 217 | 218 | # Microsoft Azure Emulator 219 | ecf/ 220 | rcf/ 221 | 222 | # Windows Store app package directories and files 223 | AppPackages/ 224 | BundleArtifacts/ 225 | Package.StoreAssociation.xml 226 | _pkginfo.txt 227 | *.appx 228 | *.appxbundle 229 | *.appxupload 230 | 231 | # Visual Studio cache files 232 | # files ending in .cache can be ignored 233 | *.[Cc]ache 234 | # but keep track of directories ending in .cache 235 | !?*.[Cc]ache/ 236 | 237 | # Others 238 | ClientBin/ 239 | ~$* 240 | *~ 241 | *.dbmdl 242 | *.dbproj.schemaview 243 | *.jfm 244 | *.pfx 245 | *.publishsettings 246 | orleans.codegen.cs 247 | 248 | # Including strong name files can present a security risk 249 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 250 | #*.snk 251 | 252 | # Since there are multiple workflows, uncomment next line to ignore bower_components 253 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 254 | #bower_components/ 255 | 256 | # RIA/Silverlight projects 257 | Generated_Code/ 258 | 259 | # Backup & report files from converting an old project file 260 | # to a newer Visual Studio version. Backup files are not needed, 261 | # because we have git ;-) 262 | _UpgradeReport_Files/ 263 | Backup*/ 264 | UpgradeLog*.XML 265 | UpgradeLog*.htm 266 | ServiceFabricBackup/ 267 | *.rptproj.bak 268 | 269 | # SQL Server files 270 | *.mdf 271 | *.ldf 272 | *.ndf 273 | 274 | # Business Intelligence projects 275 | *.rdl.data 276 | *.bim.layout 277 | *.bim_*.settings 278 | *.rptproj.rsuser 279 | *- [Bb]ackup.rdl 280 | *- [Bb]ackup ([0-9]).rdl 281 | *- [Bb]ackup ([0-9][0-9]).rdl 282 | 283 | # Microsoft Fakes 284 | FakesAssemblies/ 285 | 286 | # GhostDoc plugin setting file 287 | *.GhostDoc.xml 288 | 289 | # Node.js Tools for Visual Studio 290 | .ntvs_analysis.dat 291 | node_modules/ 292 | 293 | # Visual Studio 6 build log 294 | *.plg 295 | 296 | # Visual Studio 6 workspace options file 297 | *.opt 298 | 299 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 300 | *.vbw 301 | 302 | # Visual Studio LightSwitch build output 303 | **/*.HTMLClient/GeneratedArtifacts 304 | **/*.DesktopClient/GeneratedArtifacts 305 | **/*.DesktopClient/ModelManifest.xml 306 | **/*.Server/GeneratedArtifacts 307 | **/*.Server/ModelManifest.xml 308 | _Pvt_Extensions 309 | 310 | # Paket dependency manager 311 | .paket/paket.exe 312 | paket-files/ 313 | 314 | # FAKE - F# Make 315 | .fake/ 316 | 317 | # CodeRush personal settings 318 | .cr/personal 319 | 320 | # Python Tools for Visual Studio (PTVS) 321 | __pycache__/ 322 | *.pyc 323 | 324 | # Cake - Uncomment if you are using it 325 | # tools/** 326 | # !tools/packages.config 327 | 328 | # Tabs Studio 329 | *.tss 330 | 331 | # Telerik's JustMock configuration file 332 | *.jmconfig 333 | 334 | # BizTalk build output 335 | *.btp.cs 336 | *.btm.cs 337 | *.odx.cs 338 | *.xsd.cs 339 | 340 | # OpenCover UI analysis results 341 | OpenCover/ 342 | 343 | # Azure Stream Analytics local run output 344 | ASALocalRun/ 345 | 346 | # MSBuild Binary and Structured Log 347 | *.binlog 348 | 349 | # NVidia Nsight GPU debugger configuration file 350 | *.nvuser 351 | 352 | # MFractors (Xamarin productivity tool) working folder 353 | .mfractor/ 354 | 355 | # Local History for Visual Studio 356 | .localhistory/ 357 | 358 | # BeatPulse healthcheck temp database 359 | healthchecksdb 360 | 361 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 362 | MigrationBackup/ 363 | 364 | # Ionide (cross platform F# VS Code tools) working folder 365 | .ionide/ 366 | 367 | # Fody - auto-generated XML schema 368 | FodyWeavers.xsd 369 | 370 | # debug log 371 | debug_*.json 372 | 373 | # dotnet run in `BBDown/` sub folder 374 | /BBDown/*.mp4 375 | /BBDown/*.xml 376 | /BBDown/*.ass 377 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | charset = utf-8 10 | # end_of_line = crlf 11 | # trim_trailing_whitespace = false 12 | # insert_final_newline = false -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | #### 1. 你使用的BBDown版本是什么?(指明 Release / Actions / DotnetTool) 3 | 。。。 4 | 5 | #### 2. 你在什么系统使用本软件?(Win/Linux/Mac) 6 | 。。。 7 | 8 | #### 3. 你使用的完整命令是什么? 9 | ``` 10 | BBDown ... 11 | ``` 12 | #### 4. 遇到了什么问题? 13 | xxx 14 | 15 | #### 5. 运行截图(最好开启`--debug`;注意自行将Cookie/Token等敏感信息隐藏) 16 | 。。。 17 | -------------------------------------------------------------------------------- /.github/workflows/build_latest.yml: -------------------------------------------------------------------------------- 1 | name: Build Latest 2 | 3 | on: [push,workflow_dispatch] 4 | 5 | env: 6 | DOTNET_SDK_VERSION: '9.0.*' 7 | ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true 8 | 9 | jobs: 10 | set-date: 11 | runs-on: ubuntu-latest 12 | outputs: 13 | date: ${{ steps.get_date.outputs.date }} 14 | steps: 15 | - name: Get Date in UTC+8 16 | id: get_date 17 | run: echo "date=$(date -u -d '8 hours' +'%Y%m%d')" >> "$GITHUB_OUTPUT" 18 | 19 | build-win-x64-arm64: 20 | runs-on: windows-latest 21 | needs: set-date 22 | 23 | steps: 24 | - uses: actions/checkout@v1 25 | 26 | - name: Set up dotnet 27 | uses: actions/setup-dotnet@v3 28 | with: 29 | dotnet-version: ${{ env.DOTNET_SDK_VERSION }} 30 | 31 | - name: Install zip 32 | run: choco install zip --no-progress --yes 33 | 34 | - name: Publish [win] 35 | run: | 36 | dotnet publish BBDown -r win-x64 -c Release -o artifact 37 | dotnet publish BBDown -r win-arm64 -c Release -o artifact-arm64 38 | 39 | - name: Package [win] 40 | run: | 41 | cd artifact 42 | zip ../BBDown_${{ needs.set-date.outputs.date }}_win-x64.zip BBDown.exe 43 | cd ../artifact-arm64 44 | zip ../BBDown_${{ needs.set-date.outputs.date }}_win-arm64.zip BBDown.exe 45 | 46 | - name: Upload Artifact [win-x64] 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: BBDown_win-x64 50 | path: BBDown_${{ needs.set-date.outputs.date }}_win-x64.zip 51 | 52 | - name: Upload Artifact [win-arm64] 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: BBDown_win-arm64 56 | path: BBDown_${{ needs.set-date.outputs.date }}_win-arm64.zip 57 | 58 | build-linux-x64-arm64: 59 | runs-on: ubuntu-latest 60 | needs: set-date 61 | 62 | steps: 63 | - name: setup deb822 repos 64 | run: | 65 | if [[ $ImageOS == "ubuntu24" ]]; then 66 | cat < deb822sources 67 | Types: deb 68 | URIs: http://archive.ubuntu.com/ubuntu/ 69 | Suites: noble 70 | Components: main restricted universe 71 | Architectures: amd64 72 | 73 | Types: deb 74 | URIs: http://security.ubuntu.com/ubuntu/ 75 | Suites: noble-security 76 | Components: main restricted universe 77 | Architectures: amd64 78 | 79 | Types: deb 80 | URIs: http://archive.ubuntu.com/ubuntu/ 81 | Suites: noble-updates 82 | Components: main restricted universe 83 | Architectures: amd64 84 | 85 | Types: deb 86 | URIs: http://azure.ports.ubuntu.com/ubuntu-ports/ 87 | Suites: noble 88 | Components: main restricted multiverse universe 89 | Architectures: arm64 90 | 91 | Types: deb 92 | URIs: http://azure.ports.ubuntu.com/ubuntu-ports/ 93 | Suites: noble-updates 94 | Components: main restricted multiverse universe 95 | Architectures: arm64 96 | EOF 97 | 98 | sudo mv deb822sources /etc/apt/sources.list.d/ubuntu.sources 99 | else 100 | sudo mv config/crosscomp-sources.list /etc/apt/sources.list 101 | fi 102 | 103 | # https://learn.microsoft.com/zh-cn/dotnet/core/deploying/native-aot/cross-compile 104 | - run: | 105 | sudo dpkg --add-architecture arm64 106 | sudo bash -c 'cat > /etc/apt/sources.list.d/arm64.list < 2 | 3 | 4 | library 5 | net9.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /BBDown.Core/Config.cs: -------------------------------------------------------------------------------- 1 | namespace BBDown.Core; 2 | 3 | public static class Config 4 | { 5 | //For WEB 6 | public static string COOKIE { get; set; } = ""; 7 | //For APP/TV 8 | public static string TOKEN { get; set; } = ""; 9 | //日志级别 10 | public static bool DEBUG_LOG { get; set; } = false; 11 | //BiliPlus Host 12 | public static string HOST { get; set; } = "api.bilibili.com"; 13 | //BiliPlus EP Host 14 | public static string EPHOST { get; set; } = "api.bilibili.com"; 15 | //Bili Tv Api Host 16 | public static string TVHOST { get; set; } = "api.snm0516.aisee.tv"; 17 | //BiliPlus Area 18 | public static string AREA { get; set; } = ""; 19 | 20 | public static string WBI { get; set; } = ""; 21 | 22 | public static readonly Dictionary qualitys = new() { 23 | {"127","8K 超高清" }, {"126","杜比视界" }, {"125","HDR 真彩" }, {"120","4K 超清" }, {"116","1080P 高帧率" }, 24 | {"112","1080P 高码率" }, {"100","智能修复" }, {"80","1080P 高清" }, {"74","720P 高帧率" }, 25 | {"64","720P 高清" }, {"48","720P 高清" }, {"32","480P 清晰" }, {"16","360P 流畅" }, 26 | {"5","144P 流畅" }, {"6","240P 流畅" } 27 | }; 28 | } -------------------------------------------------------------------------------- /BBDown.Core/DanmakuUtil.cs: -------------------------------------------------------------------------------- 1 | using static BBDown.Core.Logger; 2 | using System.Text; 3 | using System.Xml; 4 | 5 | namespace BBDown.Core; 6 | 7 | public static class DanmakuUtil 8 | { 9 | private const int MONITOR_WIDTH = 1920; //渲染字幕时的渲染范围的高度 10 | private const int MONITOR_HEIGHT = 1080; //渲染字幕时的渲染范围的高度 11 | private const int FONT_SIZE = 40; //字体大小 12 | private const double MOVE_SPEND_TIME = 8.00; //单条条滚动弹幕存在时间(控制速度) 13 | private const double TOP_SPEND_TIME = 4.00; //单条顶部或底部弹幕存在时间 14 | private const int PROTECT_LENGTH = 50; //滚动弹幕屏占百分比 15 | public static readonly DanmakuComparer comparer = new(); 16 | 17 | /*public static async Task DownloadAsync(Page p, string xmlPath, bool aria2c, string aria2cProxy) 18 | { 19 | string danmakuUrl = "https://comment.bilibili.com/" + p.cid + ".xml"; 20 | await DownloadFile(danmakuUrl, xmlPath, aria2c, aria2cProxy); 21 | }*/ 22 | 23 | public static DanmakuItem[]? ParseXml(string xmlPath) 24 | { 25 | // 解析xml文件 26 | XmlDocument xmlFile = new(); 27 | XmlReaderSettings settings = new() 28 | { 29 | IgnoreComments = true//忽略文档里面的注释 30 | }; 31 | var danmakus = new List(); 32 | using (var reader = XmlReader.Create(xmlPath, settings)) 33 | { 34 | try 35 | { 36 | xmlFile.Load(reader); 37 | } 38 | catch (Exception ex) 39 | { 40 | LogDebug("解析字幕xml时出现异常: {0}", ex.ToString()); 41 | return null; 42 | } 43 | } 44 | 45 | XmlNode? rootNode = xmlFile.SelectSingleNode("i"); 46 | if (rootNode != null) 47 | { 48 | XmlElement rootElement = (XmlElement)rootNode; 49 | XmlNodeList? dNodeList = rootElement.SelectNodes("d"); 50 | if (dNodeList != null) 51 | { 52 | foreach (XmlNode node in dNodeList) 53 | { 54 | XmlElement dElement = (XmlElement)node; 55 | string attr = dElement.GetAttribute("p").ToString(); 56 | if (attr != null) 57 | { 58 | string[] vs = attr.Split(','); 59 | if (vs.Length >= 8) 60 | { 61 | DanmakuItem danmaku = new(vs, dElement.InnerText); 62 | danmakus.Add(danmaku); 63 | } 64 | } 65 | } 66 | } 67 | } 68 | return danmakus.ToArray(); 69 | } 70 | 71 | /// 72 | /// 保存为ASS字幕文件 73 | /// 74 | /// 弹幕 75 | /// 保存路径 76 | /// 77 | public static async Task SaveAsAssAsync(DanmakuItem[] danmakus, string outputPath) 78 | { 79 | var sb = new StringBuilder(); 80 | // ASS字幕文件头 81 | sb.AppendLine("[Script Info]"); 82 | sb.AppendLine("Script Updated By: BBDown(https://github.com/nilaoda/BBDown)"); 83 | sb.AppendLine("ScriptType: v4.00+"); 84 | sb.AppendLine($"PlayResX: {MONITOR_WIDTH}"); 85 | sb.AppendLine($"PlayResY: {MONITOR_HEIGHT}"); 86 | sb.AppendLine($"Aspect Ratio: {MONITOR_WIDTH}:{MONITOR_HEIGHT}"); 87 | sb.AppendLine("Collisions: Normal"); 88 | sb.AppendLine("WrapStyle: 2"); 89 | sb.AppendLine("ScaledBorderAndShadow: yes"); 90 | sb.AppendLine("YCbCr Matrix: TV.601"); 91 | sb.AppendLine("[V4+ Styles]"); 92 | sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"); 93 | sb.AppendLine($"Style: BBDOWN_Style, 黑体, {FONT_SIZE}, &H00FFFFFF, &H00FFFFFF, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 2, 0, 7, 0, 0, 0, 0"); 94 | sb.AppendLine("[Events]"); 95 | sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); 96 | 97 | PositionController controller = new(); // 弹幕位置控制器 98 | Array.Sort(danmakus, comparer); 99 | foreach (DanmakuItem danmaku in danmakus) 100 | { 101 | int height = controller.UpdatePosition(danmaku.DanmakuMode, danmaku.Second, danmaku.Content.Length); 102 | if (height == -1) continue; 103 | string effect = ""; 104 | effect += danmaku.DanmakuMode switch 105 | { 106 | 3 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {MONITOR_HEIGHT - FONT_SIZE - height})", 107 | 2 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {height})", 108 | _ => $"\\move({MONITOR_WIDTH}, {height}, {-danmaku.Content.Length * FONT_SIZE}, {height})", 109 | }; 110 | if (danmaku.Color != "FFFFFF") 111 | { 112 | effect += $"\\c&{danmaku.Color}&"; 113 | } 114 | sb.AppendLine($"Dialogue: 2,{danmaku.StartTime},{danmaku.EndTime},BBDOWN_Style,,0000,0000,0000,,{{{effect}}}{danmaku.Content}"); 115 | } 116 | 117 | await File.WriteAllTextAsync(outputPath, sb.ToString(), Encoding.UTF8); 118 | } 119 | 120 | protected class PositionController 121 | { 122 | readonly int maxLine = MONITOR_HEIGHT * PROTECT_LENGTH / FONT_SIZE / 100; //总行数 123 | // 三个位置的弹幕队列,记录弹幕结束时间 124 | 125 | readonly List moveQueue = new(); 126 | readonly List topQueue = new(); 127 | readonly List bottomQueue = new(); 128 | 129 | public PositionController() 130 | { 131 | for (int i = 0; i < maxLine; i++) 132 | { 133 | moveQueue.Add(0.00); 134 | topQueue.Add(0.00); 135 | bottomQueue.Add(0.00); 136 | } 137 | } 138 | 139 | public int UpdatePosition(int type, double time, int length) 140 | { 141 | // 获取可用位置 142 | List vs; 143 | double displayTime = TOP_SPEND_TIME; 144 | if (type == POS_BOTTOM) 145 | { 146 | vs = bottomQueue; 147 | } 148 | else if (type == POS_TOP) 149 | { 150 | vs = topQueue; 151 | } 152 | else 153 | { 154 | vs = moveQueue; 155 | displayTime = MOVE_SPEND_TIME * (length + 5) * FONT_SIZE / (MONITOR_WIDTH + (length * MOVE_SPEND_TIME)); 156 | } 157 | for (int i = 0; i < maxLine; i++) 158 | { 159 | if (time >= vs[i]) 160 | { // 此条弹幕已结束,更新该位置信息 161 | vs[i] = time + displayTime; 162 | return i * FONT_SIZE; 163 | } 164 | } 165 | return -1; 166 | } 167 | } 168 | 169 | public class DanmakuItem 170 | { 171 | public DanmakuItem(string[] attrs, string content) 172 | { 173 | DanmakuMode = attrs[1] switch 174 | { 175 | "4" => POS_BOTTOM, 176 | "5" => POS_TOP, 177 | _ => POS_MOVE, 178 | }; 179 | try 180 | { 181 | double second = double.Parse(attrs[0]); 182 | Second = second; 183 | StartTime = ComputeTime(second); 184 | EndTime = ComputeTime(second + (DanmakuMode == 1 ? MOVE_SPEND_TIME : TOP_SPEND_TIME)); 185 | } 186 | catch (Exception e) 187 | { 188 | Log(e.Message); 189 | } 190 | FontSize = attrs[2]; 191 | try 192 | { 193 | int colorD = int.Parse(attrs[3]); 194 | Color = string.Format("{0:X6}", colorD); 195 | } 196 | catch (FormatException e) 197 | { 198 | Log(e.Message); 199 | } 200 | Timestamp = attrs[4]; 201 | Content = content; 202 | } 203 | private static string ComputeTime(double second) 204 | { 205 | int hour = (int)second / 3600; 206 | int minute = (int)(second - (hour * 3600)) / 60; 207 | second -= (hour * 3600) + (minute * 60); 208 | return hour.ToString() + string.Format(":{0:D2}:", minute) + string.Format("{0:00.00}", second); 209 | } 210 | public string Content { get; set; } = ""; 211 | // 弹幕内容 212 | public string StartTime { get; set; } = ""; 213 | // 出现时间 214 | public double Second { get; set; } = 0.00; 215 | // 出现时间(秒为单位) 216 | public string EndTime { get; set; } = ""; 217 | // 消失时间 218 | public int DanmakuMode { get; set; } = POS_MOVE; 219 | // 弹幕类型 220 | public string FontSize { get; set; } = ""; 221 | // 字号 222 | public string Color { get; set; } = ""; 223 | // 颜色 224 | public string Timestamp { get; set; } = ""; 225 | // 时间戳 226 | } 227 | 228 | public class DanmakuComparer : IComparer 229 | { 230 | public int Compare(DanmakuItem? x, DanmakuItem? y) 231 | { 232 | if (x == null) return -1; 233 | if (y == null) return 1; 234 | return x.Second.CompareTo(y.Second); 235 | } 236 | } 237 | 238 | private const int POS_MOVE = 1; //滚动弹幕 239 | private const int POS_TOP = 2; //顶部弹幕 240 | private const int POS_BOTTOM = 3; //底部弹幕 241 | } -------------------------------------------------------------------------------- /BBDown.Core/Entity/Entity.cs: -------------------------------------------------------------------------------- 1 | using BBDown.Core.Util; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace BBDown.Core.Entity; 5 | 6 | public static class Entity 7 | { 8 | public class Page 9 | { 10 | public required int index; 11 | public required string aid; 12 | public required string cid; 13 | public required string epid; 14 | public required string title; 15 | public required int dur; 16 | public required string res; 17 | public required long pubTime; 18 | public string? cover; 19 | public string? desc; 20 | public string? ownerName; 21 | public string? ownerMid; 22 | public string bvid 23 | { 24 | get => BilibiliBvConverter.Encode(long.Parse(aid)); 25 | } 26 | public List points = new(); 27 | 28 | [SetsRequiredMembers] 29 | public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime) 30 | { 31 | this.aid = aid; 32 | this.index = index; 33 | this.cid = cid; 34 | this.epid = epid; 35 | this.title = title; 36 | this.dur = dur; 37 | this.res = res; 38 | this.pubTime = pubTime; 39 | } 40 | 41 | [SetsRequiredMembers] 42 | public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover) 43 | { 44 | this.aid = aid; 45 | this.index = index; 46 | this.cid = cid; 47 | this.epid = epid; 48 | this.title = title; 49 | this.dur = dur; 50 | this.res = res; 51 | this.pubTime = pubTime; 52 | this.cover = cover; 53 | } 54 | 55 | [SetsRequiredMembers] 56 | public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover, string desc) 57 | { 58 | this.aid = aid; 59 | this.index = index; 60 | this.cid = cid; 61 | this.epid = epid; 62 | this.title = title; 63 | this.dur = dur; 64 | this.res = res; 65 | this.pubTime = pubTime; 66 | this.cover = cover; 67 | this.desc = desc; 68 | } 69 | 70 | [SetsRequiredMembers] 71 | public Page(int index, string aid, string cid, string epid, string title, int dur, string res, long pubTime, string cover, string desc, string ownerName, string ownerMid) 72 | { 73 | this.aid = aid; 74 | this.index = index; 75 | this.cid = cid; 76 | this.epid = epid; 77 | this.title = title; 78 | this.dur = dur; 79 | this.res = res; 80 | this.pubTime = pubTime; 81 | this.cover = cover; 82 | this.desc = desc; 83 | this.ownerName = ownerName; 84 | this.ownerMid = ownerMid; 85 | } 86 | 87 | [SetsRequiredMembers] 88 | public Page(int index, Page page) 89 | { 90 | this.index = index; 91 | this.aid = page.aid; 92 | this.cid = page.cid; 93 | this.epid = page.epid; 94 | this.title = page.title; 95 | this.dur = page.dur; 96 | this.res = page.res; 97 | this.pubTime = page.pubTime; 98 | this.cover = page.cover; 99 | this.ownerName = page.ownerName; 100 | this.ownerMid = page.ownerMid; 101 | } 102 | 103 | public override bool Equals(object? obj) 104 | { 105 | return obj is Page page && 106 | aid == page.aid && 107 | cid == page.cid && 108 | epid == page.epid; 109 | } 110 | 111 | public override int GetHashCode() 112 | { 113 | return HashCode.Combine(aid, cid, epid); 114 | } 115 | } 116 | 117 | public class ViewPoint 118 | { 119 | public required string title; 120 | public required int start; 121 | public required int end; 122 | } 123 | 124 | public class Video 125 | { 126 | public required string id; 127 | public required string dfn; 128 | public required string baseUrl; 129 | public string? res; 130 | public string? fps; 131 | public required string codecs; 132 | public long bandwith; 133 | public int dur; 134 | public double size; 135 | 136 | public override bool Equals(object? obj) 137 | { 138 | return obj is Video video && 139 | id == video.id && 140 | dfn == video.dfn && 141 | res == video.res && 142 | fps == video.fps && 143 | codecs == video.codecs && 144 | bandwith == video.bandwith && 145 | dur == video.dur; 146 | } 147 | 148 | public override int GetHashCode() 149 | { 150 | return HashCode.Combine(id, dfn, res, fps, codecs, bandwith, dur); 151 | } 152 | } 153 | 154 | public class Audio 155 | { 156 | public required string id; 157 | public required string dfn; 158 | public required string baseUrl; 159 | public required string codecs; 160 | public required long bandwith; 161 | public required int dur; 162 | 163 | public override bool Equals(object? obj) 164 | { 165 | return obj is Audio audio && 166 | id == audio.id && 167 | dfn == audio.dfn && 168 | codecs == audio.codecs && 169 | bandwith == audio.bandwith && 170 | dur == audio.dur; 171 | } 172 | 173 | public override int GetHashCode() 174 | { 175 | return HashCode.Combine(id, dfn, codecs, bandwith, dur); 176 | } 177 | } 178 | 179 | public class Subtitle 180 | { 181 | public required string lan; 182 | public required string url; 183 | public required string path; 184 | } 185 | 186 | public class Clip 187 | { 188 | public required int index; 189 | public required long from; 190 | public required long to; 191 | } 192 | 193 | public class AudioMaterial 194 | { 195 | public required string title; 196 | public required string personName; 197 | public required string path; 198 | 199 | [SetsRequiredMembers] 200 | public AudioMaterial(string title, string personName, string path) 201 | { 202 | this.title = title; 203 | this.personName = personName; 204 | this.path = path; 205 | } 206 | 207 | [SetsRequiredMembers] 208 | public AudioMaterial(AudioMaterialInfo audioMaterialInfo) 209 | { 210 | this.title = audioMaterialInfo.title; 211 | this.personName = audioMaterialInfo.personName; 212 | this.path = audioMaterialInfo.path; 213 | } 214 | } 215 | 216 | public class AudioMaterialInfo 217 | { 218 | public required string title; 219 | public required string personName; 220 | public required string path; 221 | public required List