Repository: nilaoda/BBDown Branch: master Commit: 259a5558cee0 Files: 60 Total size: 308.8 KB Directory structure: gitextract_o82531in/ ├── .dockerignore ├── .editorconfig ├── .github/ │ ├── issue_template.md │ └── workflows/ │ └── build_latest.yml ├── .gitignore ├── 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 ├── 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 ├── Dockerfile ├── LICENSE ├── README.md └── json-api-doc.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Rider .idea # macOS shit .DS_Store # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd # debug log debug_*.json # dotnet run in `BBDown/` sub folder /BBDown/*.mp4 /BBDown/*.xml /BBDown/*.ass ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 4 charset = utf-8 # end_of_line = crlf # trim_trailing_whitespace = false # insert_final_newline = false ================================================ FILE: .github/issue_template.md ================================================ #### 1. 你使用的BBDown版本是什么?(指明 Release / Actions / DotnetTool) 。。。 #### 2. 你在什么系统使用本软件?(Win/Linux/Mac) 。。。 #### 3. 你使用的完整命令是什么? ``` BBDown ... ``` #### 4. 遇到了什么问题? xxx #### 5. 运行截图(最好开启`--debug`;注意自行将Cookie/Token等敏感信息隐藏) 。。。 ================================================ FILE: .github/workflows/build_latest.yml ================================================ name: Build Latest on: [push,workflow_dispatch] env: DOTNET_SDK_VERSION: "9.0.306" ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true jobs: set-date: runs-on: ubuntu-latest outputs: date: ${{ steps.get_date.outputs.date }} steps: - name: Get Date in UTC+8 id: get_date run: echo "date=$(date -u -d '8 hours' +'%Y%m%d')" >> "$GITHUB_OUTPUT" build-win-x64-arm64: runs-on: windows-latest needs: set-date steps: - uses: actions/checkout@v1 - name: Set up dotnet uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_SDK_VERSION }} - name: Install zip run: choco install zip --no-progress --yes - name: Publish [win] run: | dotnet publish BBDown -r win-x64 -c Release -o artifact dotnet publish BBDown -r win-arm64 -c Release -o artifact-arm64 - name: Package [win] run: | cd artifact zip ../BBDown_${{ needs.set-date.outputs.date }}_win-x64.zip BBDown.exe cd ../artifact-arm64 zip ../BBDown_${{ needs.set-date.outputs.date }}_win-arm64.zip BBDown.exe - name: Upload Artifact [win-x64] uses: actions/upload-artifact@v4 with: name: BBDown_win-x64 path: BBDown_${{ needs.set-date.outputs.date }}_win-x64.zip - name: Upload Artifact [win-arm64] uses: actions/upload-artifact@v4 with: name: BBDown_win-arm64 path: BBDown_${{ needs.set-date.outputs.date }}_win-arm64.zip build-linux-x64-arm64: runs-on: ubuntu-latest needs: set-date steps: - uses: actions/checkout@v1 - name: Build x64 in Ubuntu 18.04 container (for glibc 2.27 compatibility) run: | # 在 Ubuntu 18.04 容器中执行完整构建流程 docker run --rm \ -v "$PWD:/workspace" \ -w /workspace \ ubuntu:18.04 \ bash -c " set -e # 安装编译和运行依赖 apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y wget build-essential clang libicu-dev zlib1g libcurl4-openssl-dev libkrb5-dev # 下载并安装 .NET SDK DOTNET_SDK_VERSION='${{ env.DOTNET_SDK_VERSION }}' DOTNET_SDK_URL=\"https://builds.dotnet.microsoft.com/dotnet/Sdk/\${DOTNET_SDK_VERSION}/dotnet-sdk-\${DOTNET_SDK_VERSION}-linux-x64.tar.gz\" wget -nv \"\$DOTNET_SDK_URL\" -O dotnet-sdk.tar.gz mkdir -p /opt/dotnet tar -xzf dotnet-sdk.tar.gz -C /opt/dotnet export PATH=\"/opt/dotnet:\$PATH\" # 编译 Native AOT 输出到挂载的 artifact 目录 dotnet publish BBDown -r linux-x64 -c Release -o /workspace/artifact " - name: Build arm64 in Ubuntu 18.04 container (for glibc 2.27 compatibility) run: | # 在 Ubuntu 18.04 容器中执行完整构建流程 docker run --rm \ -v "$PWD:/workspace" \ -w /workspace \ mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-cross-arm64-20220312201346-b2c2436 \ bash -c " set -e # 下载并安装 .NET SDK DOTNET_SDK_VERSION='${{ env.DOTNET_SDK_VERSION }}' DOTNET_SDK_URL=\"https://builds.dotnet.microsoft.com/dotnet/Sdk/\${DOTNET_SDK_VERSION}/dotnet-sdk-\${DOTNET_SDK_VERSION}-linux-x64.tar.gz\" wget -nv \"\$DOTNET_SDK_URL\" -O dotnet-sdk.tar.gz mkdir -p /opt/dotnet tar -xzf dotnet-sdk.tar.gz -C /opt/dotnet export PATH=\"/opt/dotnet:\$PATH\" # 编译 Native AOT 输出到挂载的 artifact 目录 dotnet publish BBDown -r linux-arm64 -c Release -p:StripSymbols=true -p:CppCompilerAndLinker=clang-9 -p:SysRoot=/crossrootfs/arm64 -o /workspace/artifact-arm64 " - name: Package [linux] run: | cd artifact zip ../BBDown_${{ needs.set-date.outputs.date }}_linux-x64.zip BBDown cd ../artifact-arm64 zip ../BBDown_${{ needs.set-date.outputs.date }}_linux-arm64.zip BBDown - name: Upload Artifact [linux-x64] uses: actions/upload-artifact@v4 with: name: BBDown_linux-x64 path: BBDown_${{ needs.set-date.outputs.date }}_linux-x64.zip - name: Upload Artifact[linux-arm64] uses: actions/upload-artifact@v4 with: name: BBDown_linux-arm64 path: BBDown_${{ needs.set-date.outputs.date }}_linux-arm64.zip build-mac-x64-arm64: runs-on: macos-latest needs: set-date steps: - uses: actions/checkout@v1 - name: Set up dotnet uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_SDK_VERSION }} - name: Publish [osx] run: | dotnet publish BBDown -r osx-x64 -c Release -o artifact dotnet publish BBDown -r osx-arm64 -c Release -o artifact-arm64 - name: Package [osx] run: | cd artifact zip ../BBDown_${{ needs.set-date.outputs.date }}_osx-x64.zip BBDown cd ../artifact-arm64 zip ../BBDown_${{ needs.set-date.outputs.date }}_osx-arm64.zip BBDown - name: Upload Artifact [osx-x64] uses: actions/upload-artifact@v4 with: name: BBDown_osx-x64 path: BBDown_${{ needs.set-date.outputs.date }}_osx-x64.zip - name: Upload Artifact [osx-arm64] uses: actions/upload-artifact@v4 with: name: BBDown_osx-arm64 path: BBDown_${{ needs.set-date.outputs.date }}_osx-arm64.zip ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Rider .idea # macOS shit .DS_Store # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd # debug log debug_*.json # dotnet run in `BBDown/` sub folder /BBDown/*.mp4 /BBDown/*.xml /BBDown/*.ass ================================================ FILE: BBDown/BBDown.csproj ================================================ Exe net9.0 MIT 1.6.3 BBDown是一个免费且便捷高效的哔哩哔哩下载/解析软件. https://github.com/nilaoda/BBDown true BBDown ./nupkg enable ================================================ FILE: BBDown/BBDownApiServer.cs ================================================ using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using BBDown.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; namespace BBDown; public class BBDownApiServer { private WebApplication? app; private readonly List runningTasks = []; private readonly List finishedTasks = []; public void SetUpServer() { if (app is not null) return; var builder = WebApplication.CreateSlimBuilder(); builder.Services.ConfigureHttpJsonOptions((options) => { options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(options.SerializerOptions.TypeInfoResolver, AppJsonSerializerContext.Default); }); builder.Services.AddCors((options) => { options.AddPolicy("AllowAnyOrigin", policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); app = builder.Build(); app.UseCors("AllowAnyOrigin"); var taskStatusApi = app.MapGroup("/get-tasks"); taskStatusApi.MapGet("/", handler: () => Results.Json(new DownloadTaskCollection(runningTasks, finishedTasks), AppJsonSerializerContext.Default.DownloadTaskCollection)); taskStatusApi.MapGet("/running", handler: () => Results.Json(runningTasks, AppJsonSerializerContext.Default.ListDownloadTask)); taskStatusApi.MapGet("/finished", handler: () => Results.Json(finishedTasks, AppJsonSerializerContext.Default.ListDownloadTask)); taskStatusApi.MapGet("/{id}", (string id) => { var task = finishedTasks.FirstOrDefault(a => a.Aid == id); var rtask = runningTasks.FirstOrDefault(a => a.Aid == id); if (rtask is not null) task = rtask; if (task is null) { return Results.NotFound(); } return Results.Json(task, AppJsonSerializerContext.Default.DownloadTask); }); app.MapPost("/add-task", (MyOptionBindingResult bindingResult) => { if (!bindingResult.IsValid) { //var exception = bindingResult.Exception; return Results.BadRequest("输入有误"); } var req = bindingResult.Result; _ = AddDownloadTaskAsync(req) .ContinueWith(async task => { // send request to callback webhook if (string.IsNullOrEmpty(req.CallBackWebHook)) { return; } string callback = req.CallBackWebHook; var client = new HttpClient(); var downloadTask = await task; string? jsonContent = JsonSerializer.Serialize(downloadTask, AppJsonSerializerContext.Default.DownloadTask); try { await client.PostAsync(callback, new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json")); } catch (System.Exception e) { Logger.LogDebug("回调失败", e.Message); } }); return Results.Ok(); }); var finishedRemovalApi = app.MapGroup("remove-finished"); finishedRemovalApi.MapGet("/", () => { finishedTasks.RemoveAll(t => true); return Results.Ok(); }); finishedRemovalApi.MapGet("/failed", () => { finishedTasks.RemoveAll(t => !t.IsSuccessful); return Results.Ok(); }); finishedRemovalApi.MapGet("/{id}", (string id) => { finishedTasks.RemoveAll(t => t.Aid == id); return Results.Ok(); }); } public void Run(string url) { if (app is null) return; bool result = Uri.TryCreate(url, UriKind.Absolute, out Uri? uriResult) && uriResult.Scheme == Uri.UriSchemeHttp; if (!result) { Console.BackgroundColor = ConsoleColor.Red; Console.ForegroundColor = ConsoleColor.White; Console.WriteLine($"{url}不是合法的http URL,url示例:http://0.0.0.0:5000"); Console.WriteLine("如果您需要https,请额外配置反向代理"); Console.ResetColor(); Console.WriteLine(); Thread.Sleep(1); Environment.Exit(1); } app.Run(url); } private async Task AddDownloadTaskAsync(MyOption option) { var aid = await BBDownUtil.GetAvIdAsync(option.Url); DownloadTask? runningTask = runningTasks.FirstOrDefault(task => task.Aid == aid); if (runningTask is not null) { return runningTask; }; var task = new DownloadTask(aid, option.Url, DateTimeOffset.Now.ToUnixTimeSeconds()); runningTasks.Add(task); try { var (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, aidOri, delay) = Program.SetUpWork(option); var (fetchedAid, vInfo, apiType) = await Program.GetVideoInfoAsync(option, aidOri, input); task.Title = vInfo.Title; task.Pic = vInfo.Pic; task.VideoPubTime = vInfo.PubTime; await Program.DownloadPagesAsync(option, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, fetchedAid, delay, apiType, task); task.IsSuccessful = true; } catch (Exception e) { Console.BackgroundColor = ConsoleColor.Red; Console.ForegroundColor = ConsoleColor.White; Console.WriteLine($"{aid}下载失败"); var msg = Config.DEBUG_LOG ? e.ToString() : e.Message; Console.Write($"{msg}{Environment.NewLine}请尝试升级到最新版本后重试!"); Console.ResetColor(); Console.WriteLine(); } task.TaskFinishTime = DateTimeOffset.Now.ToUnixTimeSeconds(); if (task.IsSuccessful) { task.Progress = 1f; task.DownloadSpeed = (double)(task.TotalDownloadedBytes / (task.TaskFinishTime - task.TaskCreateTime)); } runningTasks.Remove(task); finishedTasks.Add(task); return task; } } public record DownloadTask(string Aid, string Url, long TaskCreateTime) { [JsonInclude] public string? Title = null; [JsonInclude] public string? Pic = null; [JsonInclude] public long? VideoPubTime = null; [JsonInclude] public long? TaskFinishTime = null; [JsonInclude] public double Progress = 0f; [JsonInclude] public double DownloadSpeed = 0f; [JsonInclude] public double TotalDownloadedBytes = 0f; [JsonInclude] public bool IsSuccessful = false; [JsonInclude] public List SavePaths = new(); }; public record DownloadTaskCollection(List Running, List Finished); record struct MyOptionBindingResult(T? Result, Exception? Exception) { public bool IsValid => Exception is null; public static async ValueTask> BindAsync(HttpContext httpContext) { try { JsonTypeInfo? jsonTypeInfo = SourceGenerationContext.Default.GetTypeInfo(typeof(T)); if (jsonTypeInfo is null) { return new(default, new InvalidOperationException($"Cannot find TypeInfo for type {typeof(T)}")); } var item = await httpContext.Request.ReadFromJsonAsync(jsonTypeInfo); if (item is null) return new(default, new NoNullAllowedException()); return new((T)item, null); } catch (Exception ex) { return new(default, ex); } } } [JsonSerializable(typeof(ProblemDetails))] [JsonSerializable(typeof(ValidationProblemDetails))] [JsonSerializable(typeof(HttpValidationProblemDetails))] [JsonSerializable(typeof(DownloadTask))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(DownloadTaskCollection))] public partial class AppJsonSerializerContext : JsonSerializerContext { } [JsonSerializable(typeof(MyOption))] [JsonSerializable(typeof(ServeRequestOptions))] internal partial class SourceGenerationContext : JsonSerializerContext { } ================================================ FILE: BBDown/BBDownAria2c.cs ================================================ using System.Diagnostics; using System.IO; using System.Threading.Tasks; namespace BBDown; static class BBDownAria2c { public static string ARIA2C = "aria2c"; public static async Task RunCommandCodeAsync(string command, string args) { using Process p = new(); p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = false; p.StartInfo.FileName = command; p.StartInfo.Arguments = args; p.Start(); await p.WaitForExitAsync(); return p.ExitCode; } public static async Task DownloadFileByAria2cAsync(string url, string path, string extraArgs) { var headerArgs = ""; if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android")) headerArgs += " --header=\"Referer: https://www.bilibili.com\""; headerArgs += " --header=\"User-Agent: Mozilla/5.0\""; headerArgs += $" --header=\"Cookie: {Core.Config.COOKIE}\""; await RunCommandCodeAsync(ARIA2C, $" --auto-file-renaming=false --download-result=hide --allow-overwrite=true --console-log-level=warn -x16 -s16 -j16 -k5M {headerArgs} {extraArgs} \"{url}\" -d \"{Path.GetDirectoryName(path)}\" -o \"{Path.GetFileName(path)}\""); } } ================================================ FILE: BBDown/BBDownConfigParser.cs ================================================ using System; using System.Collections.Generic; using System.CommandLine.Parsing; using System.CommandLine; using System.IO; using System.Linq; using static BBDown.Core.Logger; namespace BBDown; internal static class BBDownConfigParser { public static void HandleConfig(List newArgsList, RootCommand rootCommand) { try { var configPath = newArgsList.Contains("--config-file") ? newArgsList.ElementAt(newArgsList.IndexOf("--config-file") + 1) : Path.Combine(Program.APP_DIR, "BBDown.config"); if (File.Exists(configPath)) { Log($"加载配置文件: {configPath}"); var configArgs = File .ReadAllLines(configPath) .Where(s => !string.IsNullOrEmpty(s) && !s.StartsWith('#')) .SelectMany(s => { var trimLine = s.Trim(); if (trimLine.StartsWith('-') && trimLine.Contains(' ')) { var spaceIndex = trimLine.IndexOf(' '); var paramsGroup = new[] { trimLine[..spaceIndex], trimLine[spaceIndex..] }; return paramsGroup.Where(s => !string.IsNullOrEmpty(s)).Select(s => s.Trim(' ').Trim('\"')); } return [trimLine.Trim('\"')]; } ); var configArgsResult = rootCommand.Parse(configArgs.ToArray()); foreach (var item in configArgsResult.CommandResult.Children) { if (item is OptionResult o) { if (!newArgsList.Contains("--" + o.Option.Name)) { newArgsList.Add("--" + o.Option.Name); newArgsList.AddRange(o.Tokens.Select(t => t.Value)); } } } //命令行的优先级>配置文件优先级 LogDebug("新的命令行参数: " + string.Join(" ", newArgsList)); } } catch (Exception) { LogError("配置文件读取异常,忽略"); } } } ================================================ FILE: BBDown/BBDownDownloadUtil.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net; using System.Threading.Tasks; using static BBDown.Core.Entity.Entity; using static BBDown.Core.Logger; using static BBDown.Core.Util.HTTPUtil; using System.Collections.Concurrent; namespace BBDown; internal static class BBDownDownloadUtil { public class DownloadConfig { public bool UseAria2c { get; set; } = false; public string Aria2cArgs { get; set; } = string.Empty; public bool ForceHttp { get; set; } = false; public bool MultiThread { get; set; } = false; public DownloadTask? RelatedTask { get; set; } = null; } private static async Task RangeDownloadToTmpAsync(int id, string url, string tmpName, long fromPosition, long? toPosition, Action onProgress, bool failOnRangeNotSupported = false) { DateTimeOffset? lastTime = File.Exists(tmpName) ? new FileInfo(tmpName).LastWriteTimeUtc : null; using var fileStream = new FileStream(tmpName, FileMode.OpenOrCreate); fileStream.Seek(0, SeekOrigin.End); if (toPosition > 0 && fileStream.Position == toPosition - fromPosition + 1) { // 已下载完成 直接汇报进度并跳过下载 onProgress(id, fileStream.Position, fileStream.Position); return; } var downloadedBytes = fromPosition + fileStream.Position; using var httpRequestMessage = new HttpRequestMessage(); if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android")) httpRequestMessage.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com"); httpRequestMessage.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0"); httpRequestMessage.Headers.TryAddWithoutValidation("Cookie", Core.Config.COOKIE); httpRequestMessage.Headers.Range = new(downloadedBytes, toPosition); httpRequestMessage.Headers.IfRange = lastTime != null ? new(lastTime.Value) : null; httpRequestMessage.RequestUri = new(url); using var response = (await AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode(); if (response.StatusCode == HttpStatusCode.OK) // server doesn't response a partial content { if (failOnRangeNotSupported && (downloadedBytes > 0 || toPosition != null)) throw new NotSupportedException("Range request is not supported."); downloadedBytes = 0; fileStream.Seek(0, SeekOrigin.Begin); } using var stream = await response.Content.ReadAsStreamAsync(); var totalBytes = downloadedBytes + (response.Content.Headers.ContentLength ?? long.MaxValue - downloadedBytes); const int blockSize = 1048576 / 4; var buffer = new byte[blockSize]; while (downloadedBytes < totalBytes) { var recevied = await stream.ReadAsync(buffer); if (recevied == 0) break; await fileStream.WriteAsync(buffer.AsMemory(0, recevied)); await fileStream.FlushAsync(); downloadedBytes += recevied; onProgress(id, downloadedBytes - fromPosition, totalBytes); } if (response.Content.Headers.ContentLength != null && (response.Content.Headers.ContentLength != new FileInfo(tmpName).Length)) throw new Exception("Retry..."); } public static async Task DownloadFileAsync(string url, string path, DownloadConfig config) { if (string.IsNullOrEmpty(url)) return; if (config.ForceHttp) url = ReplaceUrl(url); LogDebug("Start downloading: {0}", url); string desDir = Path.GetDirectoryName(path)!; if (!string.IsNullOrEmpty(desDir) && !Directory.Exists(desDir)) Directory.CreateDirectory(desDir); if (config.UseAria2c) { await BBDownAria2c.DownloadFileByAria2cAsync(url, path, config.Aria2cArgs); if (File.Exists(path + ".aria2") || !File.Exists(path)) throw new Exception("aria2下载可能存在错误"); Console.WriteLine(); return; } int retry = 0; string tmpName = Path.Combine(desDir, Path.GetFileNameWithoutExtension(path) + ".tmp"); reDown: try { using var progress = new ProgressBar(config.RelatedTask); await RangeDownloadToTmpAsync(0, url, tmpName, 0, null, (_, downloaded, total) => progress.Report((double)downloaded / total, downloaded)); File.Move(tmpName, path, true); } catch (Exception) { if (++retry == 3) throw; goto reDown; } } public static async Task MultiThreadDownloadFileAsync(string url, string path, DownloadConfig config) { if (config.ForceHttp) url = ReplaceUrl(url); LogDebug("Start downloading: {0}", url); if (config.UseAria2c) { await BBDownAria2c.DownloadFileByAria2cAsync(url, path, config.Aria2cArgs); if (File.Exists(path + ".aria2") || !File.Exists(path)) throw new Exception("aria2下载可能存在错误"); Console.WriteLine(); return; } long fileSize = await GetFileSizeAsync(url); LogDebug("文件大小:{0} bytes", fileSize); //已下载过, 跳过下载 if (File.Exists(path) && new FileInfo(path).Length == fileSize) { LogDebug("文件已下载过, 跳过下载"); return; } List allClips = GetAllClips(url, fileSize); int total = allClips.Count; LogDebug("分段数量:{0}", total); ConcurrentDictionary clipProgress = new(); foreach (var i in allClips) clipProgress[i.index] = 0; using var progress = new ProgressBar(config.RelatedTask); progress.Report(0); await Parallel.ForEachAsync(allClips, async (clip, _) => { int retry = 0; string tmp = Path.Combine(Path.GetDirectoryName(path)!, clip.index.ToString("00000") + "_" + Path.GetFileNameWithoutExtension(path) + (Path.GetExtension(path).EndsWith(".mp4") ? ".vclip" : ".aclip")); reDown: try { await RangeDownloadToTmpAsync(clip.index, url, tmp, clip.from, clip.to == -1 ? null : clip.to, (index, downloaded, _) => { clipProgress[index] = downloaded; progress.Report((double)clipProgress.Values.Sum() / fileSize, clipProgress.Values.Sum()); }, true); } catch (NotSupportedException) { if (++retry == 3) throw new Exception($"服务器可能并不支持多线程下载, 请使用 --multi-thread false 关闭多线程"); goto reDown; } catch (Exception) { if (++retry == 3) throw new Exception($"Failed to download clip {clip.index}"); goto reDown; } }); } //此函数主要是切片下载逻辑 private static List GetAllClips(string url, long fileSize) { List clips = []; int index = 0; long counter = 0; int perSize = 20 * 1024 * 1024; while (fileSize > 0) { Clip c = new() { index = index, from = counter, to = counter + perSize }; //没到最后 if (fileSize - perSize > 0) { fileSize -= perSize; counter += perSize + 1; index++; clips.Add(c); } //已到最后 else { c.to = -1; clips.Add(c); break; } } return clips; } private static async Task GetFileSizeAsync(string url) { using var httpRequestMessage = new HttpRequestMessage(); if (!url.Contains("platform=android_tv_yst") && !url.Contains("platform=android")) httpRequestMessage.Headers.TryAddWithoutValidation("Referer", "https://www.bilibili.com"); httpRequestMessage.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0"); httpRequestMessage.Headers.TryAddWithoutValidation("Cookie", Core.Config.COOKIE); httpRequestMessage.RequestUri = new(url); var response = (await AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode(); long totalSizeBytes = response.Content.Headers.ContentLength ?? 0; return totalSizeBytes; } /// /// 将下载地址强制转换为HTTP /// /// /// private static string ReplaceUrl(string url) { if (url.Contains(".mcdn.bilivideo.cn:")) { LogDebug("对[*.mcdn.bilivideo.cn:xxx]域名不做处理"); return url; } LogDebug("将https更改为http"); return url.Replace("https:", "http:"); } } ================================================ FILE: BBDown/BBDownEnums.cs ================================================ using System; using System.Linq; namespace BBDown; public enum BBDownDanmakuFormat { Xml, Ass, } public static class BBDownDanmakuFormatInfo { // 默认 public static BBDownDanmakuFormat[] DefaultFormats = [BBDownDanmakuFormat.Xml, BBDownDanmakuFormat.Ass]; public static string[] DefaultFormatsNames = DefaultFormats.Select(f => f.ToString().ToLower()).ToArray(); // 可选项 public static string[] AllFormatNames = Enum.GetNames(typeof(BBDownDanmakuFormat)).Select(f => f.ToLower()).ToArray(); public static BBDownDanmakuFormat FromFormatName(string formatName) { return formatName switch { "xml" => BBDownDanmakuFormat.Xml, "ass" => BBDownDanmakuFormat.Ass, _ => BBDownDanmakuFormat.Xml, }; } } ================================================ FILE: BBDown/BBDownLoginUtil.cs ================================================ using QRCoder; using System; using System.IO; using System.Threading.Tasks; using static BBDown.BBDownUtil; using static BBDown.Core.Logger; using System.Text; using System.Text.Json; using System.Net.Http; using BBDown.Core.Util; namespace BBDown; internal static class BBDownLoginUtil { public static async Task GetLoginStatusAsync(string qrcodeKey) { string queryUrl = $"https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key={qrcodeKey}&source=main-fe-header"; return await HTTPUtil.GetWebSourceAsync(queryUrl); } public static async Task LoginWEB() { try { Log("获取登录地址..."); string loginUrl = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header"; string url = JsonDocument.Parse(await HTTPUtil.GetWebSourceAsync(loginUrl)).RootElement.GetProperty("data").GetProperty("url").ToString(); string qrcodeKey = GetQueryString("qrcode_key", url); //Log(oauthKey); //Log(url); bool flag = false; Log("生成二维码..."); QRCodeGenerator qrGenerator = new(); QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); PngByteQRCode pngByteCode = new(qrCodeData); await File.WriteAllBytesAsync("qrcode.png", pngByteCode.GetGraphic(7)); Log("生成二维码成功: qrcode.png, 请打开并扫描, 或扫描打印的二维码"); var consoleQRCode = new ConsoleQRCode(qrCodeData); consoleQRCode.GetGraphic(); while (true) { await Task.Delay(1000); string w = await GetLoginStatusAsync(qrcodeKey); int code = JsonDocument.Parse(w).RootElement.GetProperty("data").GetProperty("code").GetInt32(); if (code == 86038) { LogColor("二维码已过期, 请重新执行登录指令."); break; } else if (code == 86101) //等待扫码 { continue; } else if (code == 86090) //等待确认 { if (!flag) { Log("扫码成功, 请确认..."); flag = !flag; } } else { string cc = JsonDocument.Parse(w).RootElement.GetProperty("data").GetProperty("url").ToString(); Log("登录成功: SESSDATA=" + GetQueryString("SESSDATA", cc)); //导出cookie, 转义英文逗号 否则部分场景会出问题 await File.WriteAllTextAsync(Path.Combine(Program.APP_DIR, "BBDown.data"), cc[(cc.IndexOf('?') + 1)..].Replace("&", ";").Replace(",", "%2C")); File.Delete("qrcode.png"); break; } } } catch (Exception e) { LogError(e.Message); } } public static async Task LoginTV() { try { string loginUrl = "https://passport.snm0516.aisee.tv/x/passport-tv-login/qrcode/auth_code"; string pollUrl = "https://passport.bilibili.com/x/passport-tv-login/qrcode/poll"; var parms = GetTVLoginParms(); Log("获取登录地址..."); byte[] responseArray = await (await HTTPUtil.AppHttpClient.PostAsync(loginUrl, new FormUrlEncodedContent(parms.ToDictionary()))).Content.ReadAsByteArrayAsync(); string web = Encoding.UTF8.GetString(responseArray); string url = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("url").ToString(); string authCode = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("auth_code").ToString(); Log("生成二维码..."); QRCodeGenerator qrGenerator = new(); QRCodeData qrCodeData = qrGenerator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q); PngByteQRCode pngByteCode = new(qrCodeData); await File.WriteAllBytesAsync("qrcode.png", pngByteCode.GetGraphic(7)); Log("生成二维码成功: qrcode.png, 请打开并扫描, 或扫描打印的二维码"); var consoleQRCode = new ConsoleQRCode(qrCodeData); consoleQRCode.GetGraphic(); parms.Set("auth_code", authCode); parms.Set("ts", GetTimeStamp(true)); parms.Remove("sign"); parms.Add("sign", GetSign(ToQueryString(parms))); while (true) { await Task.Delay(1000); responseArray = await (await HTTPUtil.AppHttpClient.PostAsync(pollUrl, new FormUrlEncodedContent(parms.ToDictionary()))).Content.ReadAsByteArrayAsync(); web = Encoding.UTF8.GetString(responseArray); string code = JsonDocument.Parse(web).RootElement.GetProperty("code").ToString(); if (code == "86038") { LogColor("二维码已过期, 请重新执行登录指令."); break; } else if (code == "86039") //等待扫码 { continue; } else { string cc = JsonDocument.Parse(web).RootElement.GetProperty("data").GetProperty("access_token").ToString(); Log("登录成功: AccessToken=" + cc); //导出cookie await File.WriteAllTextAsync(Path.Combine(Program.APP_DIR, "BBDownTV.data"), "access_token=" + cc); File.Delete("qrcode.png"); break; } } } catch (Exception e) { LogError(e.Message); } } } ================================================ FILE: BBDown/BBDownMuxer.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using static BBDown.Core.Entity.Entity; using static BBDown.BBDownUtil; using static BBDown.Core.Util.SubUtil; using static BBDown.Core.Logger; using System.IO; using BBDown.Core; using System.Runtime.InteropServices; namespace BBDown; static partial class BBDownMuxer { public static string FFMPEG = "ffmpeg"; public static string MP4BOX = "mp4box"; private static int RunExe(string app, string parms, bool customBin = false) { int code = 0; Process p = new(); p.StartInfo.FileName = app; p.StartInfo.Arguments = parms; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardError = true; p.StartInfo.CreateNoWindow = false; p.ErrorDataReceived += delegate (object sendProcess, DataReceivedEventArgs output) { if (!string.IsNullOrWhiteSpace(output.Data)) Log(output.Data); }; p.StartInfo.StandardErrorEncoding = Encoding.UTF8; p.Start(); p.BeginErrorReadLine(); p.WaitForExit(); p.Close(); p.Dispose(); return code; } private static string EscapeString(string str) { return string.IsNullOrEmpty(str) ? str : str.Replace("\"", "'").Replace("\\", "\\\\"); } private static int MuxByMp4box(string url, string videoPath, string audioPath, string outPath, string desc, string title, string author, string episodeId, string pic, string lang, List? subs, bool audioOnly, bool videoOnly, List? points) { StringBuilder inputArg = new(); StringBuilder metaArg = new(); int nowId = 0; inputArg.Append(" -inter 500 -noprog "); if (!string.IsNullOrEmpty(videoPath)) { inputArg.Append($" -add \"{videoPath}#trackID={(audioOnly && audioPath == "" ? "2" : "1")}:name=\" "); nowId++; } if (!string.IsNullOrEmpty(audioPath)) { inputArg.Append($" -add \"{audioPath}:lang={(lang == "" ? "und" : lang)}\" "); nowId++; } if (points != null && points.Any()) { var meta = GetMp4boxMetaString(points); var metaFile = Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters"); File.WriteAllText(metaFile, meta); inputArg.Append($" -chap \"{metaFile}\" "); } if (!string.IsNullOrEmpty(pic)) metaArg.Append($":cover=\"{pic}\""); if (!string.IsNullOrEmpty(episodeId)) metaArg.Append($":album=\"{title}\":title=\"{episodeId}\""); else metaArg.Append($":title=\"{title}\""); metaArg.Append($":sdesc=\"{desc}\""); metaArg.Append($":comment=\"{url}\""); metaArg.Append($":artist=\"{author}\""); if (subs != null) { for (int i = 0; i < subs.Count; i++) { if (File.Exists(subs[i].path) && File.ReadAllText(subs[i].path!) != "") { nowId++; inputArg.Append($" -add \"{subs[i].path}#trackID=1:name=:hdlr=sbtl:lang={GetSubtitleCode(subs[i].lan).Item1}\" "); inputArg.Append($" -udta {nowId}:type=name:str=\"{GetSubtitleCode(subs[i].lan).Item2}\" "); } } } //----分析完毕 var arguments = (Config.DEBUG_LOG ? " -v " : "") + inputArg + (metaArg.ToString() == "" ? "" : " -itags tool=" + metaArg) + $" -new -- \"{outPath}\""; LogDebug("mp4box命令: {0}", arguments); return RunExe(MP4BOX, arguments, MP4BOX != "mp4box"); } public static int MuxAV(bool useMp4box, string bvid, string videoPath, string audioPath, List audioMaterial, string outPath, string desc = "", string title = "", string author = "", string episodeId = "", string pic = "", string lang = "", List? subs = null, bool audioOnly = false, bool videoOnly = false, List? points = null, long pubTime = 0, bool simplyMux = false, bool isHevc = false) { if (audioOnly && audioPath != "") videoPath = ""; if (videoOnly) audioPath = ""; desc = EscapeString(desc); title = EscapeString(title); episodeId = EscapeString(episodeId); var url = $"https://www.bilibili.com/video/{bvid}/"; if (useMp4box) { return MuxByMp4box(url, videoPath, audioPath, outPath, desc, title, author, episodeId, pic, lang, subs, audioOnly, videoOnly, points); } if (outPath.Contains('/') && ! Directory.Exists(Path.GetDirectoryName(outPath))) Directory.CreateDirectory(Path.GetDirectoryName(outPath)!); //----分析并生成-i参数 StringBuilder inputArg = new(); StringBuilder metaArg = new(); byte inputCount = 0; foreach (string path in new[] { videoPath, audioPath }) { if (!string.IsNullOrEmpty(path)) { inputCount++; inputArg.Append($"-i \"{path}\" "); } } if (audioMaterial.Any()) { byte audioCount = 0; metaArg.Append("-metadata:s:a:0 title=\"原音频\" "); foreach (var audio in audioMaterial) { inputCount++; audioCount++; inputArg.Append($"-i \"{audio.path}\" "); if (!string.IsNullOrWhiteSpace(audio.title)) metaArg.Append($"-metadata:s:a:{audioCount} title=\"{audio.title}\" "); if (!string.IsNullOrWhiteSpace(audio.personName)) metaArg.Append($"-metadata:s:a:{audioCount} artist=\"{audio.personName}\" "); } } if (!string.IsNullOrEmpty(pic)) { inputCount++; inputArg.Append($"-i \"{pic}\" "); } if (subs != null) { for (int i = 0; i < subs.Count; i++) { if(File.Exists(subs[i].path) && File.ReadAllText(subs[i].path!) != "") { inputCount++; inputArg.Append($"-i \"{subs[i].path}\" "); metaArg.Append($"-metadata:s:s:{i} title=\"{GetSubtitleCode(subs[i].lan).Item2}\" -metadata:s:s:{i} language={GetSubtitleCode(subs[i].lan).Item1} "); } } } if (!string.IsNullOrEmpty(pic)) metaArg.Append($"-disposition:v:{(audioOnly ? "0" : "1")} attached_pic "); // var inputCount = InputRegex().Matches(inputArg.ToString()).Count; if (points != null && points.Any()) { var meta = GetFFmpegMetaString(points); var metaFile = Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters"); File.WriteAllText(metaFile, meta); inputArg.Append($"-i \"{metaFile}\" -map_chapters {inputCount} "); } inputArg.Append(string.Concat(Enumerable.Range(0, inputCount).Select(i => $"-map {i} "))); //----分析完毕 StringBuilder argsBuilder = new StringBuilder(); argsBuilder.Append($"-loglevel {(Config.DEBUG_LOG ? "verbose" : "warning")} -y "); argsBuilder.Append(inputArg); argsBuilder.Append(metaArg); if (!simplyMux) { argsBuilder.Append($"-metadata title=\"{(episodeId == "" ? title : episodeId)}\" "); argsBuilder.Append($"-metadata comment=\"{url}\" "); if (lang != "") argsBuilder.Append($"-metadata:s:a:0 language={lang} "); if (!string.IsNullOrWhiteSpace(desc)) argsBuilder.Append($"-metadata description=\"{desc}\" "); if (!string.IsNullOrEmpty(author)) argsBuilder.Append($"-metadata artist=\"{author}\" "); if (episodeId != "") argsBuilder.Append($"-metadata album=\"{title}\" "); if (pubTime != 0) argsBuilder.Append($"-metadata creation_time=\"{(DateTimeOffset.FromUnixTimeSeconds(pubTime).ToString("yyyy-MM-ddTHH:mm:ss.ffffffZ"))}\" "); } argsBuilder.Append("-c:v copy -c:a copy "); if (audioOnly && audioPath == "") argsBuilder.Append("-vn "); if (subs != null) argsBuilder.Append("-c:s mov_text "); // fix macOS hev1, see https://discussions.apple.com/thread/253081863?sortBy=rank if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && isHevc) argsBuilder.Append("-tag:v:0 hvc1 "); argsBuilder.Append($"-movflags faststart -strict unofficial -strict -2 -f mp4 -- \"{outPath}\""); string arguments = argsBuilder.ToString(); LogDebug("ffmpeg命令: {0}", arguments); return RunExe(FFMPEG, arguments, FFMPEG != "ffmpeg"); } public static void MergeFLV(string[] files, string outPath) { if (files.Length == 1) { File.Move(files[0], outPath); } else { foreach (var file in files) { var tmpFile = Path.Combine(Path.GetDirectoryName(file)!, Path.GetFileNameWithoutExtension(file) + ".ts"); var arguments = $"-loglevel warning -y -i \"{file}\" -map 0 -c copy -f mpegts -bsf:v h264_mp4toannexb \"{tmpFile}\""; LogDebug("ffmpeg命令: {0}", arguments); RunExe("ffmpeg", arguments); File.Delete(file); } var f = GetFiles(Path.GetDirectoryName(files[0])!, ".ts"); CombineMultipleFilesIntoSingleFile(f, outPath); foreach (var s in f) File.Delete(s); } } } ================================================ FILE: BBDown/BBDownUtil.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using static BBDown.Core.Entity.Entity; using static BBDown.Core.Logger; using static BBDown.Core.Util.HTTPUtil; namespace BBDown; static partial class BBDownUtil { public static async Task CheckUpdateAsync() { try { var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!; string nowVer = $"{ver.Major}.{ver.Minor}.{ver.Build}"; string redirectUrl = await GetWebLocationAsync("https://github.com/nilaoda/BBDown/releases/latest"); string latestVer = redirectUrl.Replace("https://github.com/nilaoda/BBDown/releases/tag/", ""); if (nowVer != latestVer && !latestVer.StartsWith("https")) { Console.Title = $"发现新版本:{latestVer}"; LogColor($"发现新版本:{latestVer}"); } } catch (Exception) { ; } } public static async Task GetAvIdAsync(string input) { var avid = input; if (input.StartsWith("http")) { if (input.Contains("b23.tv")) { string tmp = await GetWebLocationAsync(input); if (tmp == input) throw new Exception("无限重定向"); input = tmp; } if (input.Contains("video/av")) { avid = AvRegex().Match(input).Groups[1].Value; } else if (input.ToLower().Contains("video/bv")) { avid = GetAidByBV(BVRegex().Match(input).Groups[1].Value); } else if (input.Contains("/cheese/")) { string epId = ""; if (input.Contains("/ep")) { epId = EpRegex().Match(input).Groups[1].Value; } else if (input.Contains("/ss")) { epId = await GetEpidBySSIdAsync(SsRegex().Match(input).Groups[1].Value); } avid = $"cheese:{epId}"; } else if (input.Contains("/ep")) { string epId = EpRegex().Match(input).Groups[1].Value; avid = $"ep:{epId}"; } else if (input.Contains("/ss")) { string epId = await GetEpIdByBangumiSSIdAsync(SsRegex().Match(input).Groups[1].Value); avid = $"ep:{epId}"; } else if (input.Contains("/medialist/") && input.Contains("business_id=") && input.Contains("business=space_collection")) // 列表类型是合集 { string bizId = GetQueryString("business_id", input); avid = $"listBizId:{bizId}"; } else if (input.Contains("/medialist/") && input.Contains("business_id=") && input.Contains("business=space_series")) // 列表类型是系列 { string bizId = GetQueryString("business_id", input); avid = $"seriesBizId:{bizId}"; } else if (input.Contains("/channel/collectiondetail?sid=")) { string bizId = GetQueryString("sid", input); avid = $"listBizId:{bizId}"; } else if (input.Contains("/channel/seriesdetail?sid=")) { string bizId = GetQueryString("sid", input); avid = $"seriesBizId:{bizId}"; } // 新版个人空间合集/系列链接兼容: // 例如: // 合集: https://space.bilibili.com/392959666/lists/1560264?type=season // 系列: https://space.bilibili.com/392959666/lists/1560264?type=series else if (input.Contains("/space.bilibili.com/") && input.Contains("/lists/")) { var type = GetQueryString("type", input).ToLower(); // path 最后一个 / 后到 ? 前即为 sid var path = input.Split('?', '#')[0]; var sidPart = path[(path.LastIndexOf('/') + 1)..]; if (type == "season") { avid = $"listBizId:{sidPart}"; } else if (type == "series") { avid = $"seriesBizId:{sidPart}"; } else { // 未知类型按合集处理,至少不会识别失败 avid = $"listBizId:{sidPart}"; } } else if (input.Contains("/space.bilibili.com/") && input.Contains("/favlist")) { string mid = UidRegex().Match(input).Groups[1].Value; string fid = GetQueryString("fid", input); avid = $"favId:{fid}:{mid}"; } else if (input.Contains("/space.bilibili.com/")) { string mid = UidRegex().Match(input).Groups[1].Value; avid = $"mid:{mid}"; } else if (input.Contains("ep_id=")) { string epId = GetQueryString("ep_id", input); avid = $"ep:{epId}"; } else if (GlobalEpRegex().Match(input).Success) { string epId = GlobalEpRegex().Match(input).Groups[1].Value; avid = $"ep:{epId}"; } else if (BangumiMdRegex().Match(input).Success) { string mdId = BangumiMdRegex().Match(input).Groups[1].Value; string epId = await GetEpIdByMDAsync(mdId); avid = $"ep:{epId}"; } else { string web = await GetWebSourceAsync(input); Regex regex = StateRegex(); string json = regex.Match(web).Groups[1].Value; using var jDoc = JsonDocument.Parse(json); string epId = jDoc.RootElement.GetProperty("epList").EnumerateArray().First().GetProperty("id").ToString(); avid = $"ep:{epId}"; } } else if (input.ToLower().StartsWith("bv")) { avid = GetAidByBV(input[3..]); } else if (input.ToLower().StartsWith("av")) // av { avid = input.ToLower()[2..]; } else if (input.StartsWith("cheese/")) // ^cheese/(ep|ss)\d+ 格式 { string epId = ""; if (input.Contains("/ep")) { epId = EpRegex().Match(input).Groups[1].Value; } else if (input.Contains("/ss")) { epId = await GetEpidBySSIdAsync(SsRegex().Match(input).Groups[1].Value); } avid = $"cheese:{epId}"; } else if (input.StartsWith("ep")) { string epId = input[2..]; avid = $"ep:{epId}"; } else if (input.StartsWith("ss")) { string epId = await GetEpIdByBangumiSSIdAsync(input[2..]); avid = $"ep:{epId}"; } else if (input.StartsWith("md")) { string mdId = MdRegex().Match(input).Groups[1].Value; string epId = await GetEpIdByMDAsync(mdId); avid = $"ep:{epId}"; } else { throw new Exception("输入有误"); } return await FixAvidAsync(avid); } public static string FormatFileSize(double fileSize) { return fileSize switch { < 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)), >= 1024 * 1024 * 1024 => $"{fileSize / (1024 * 1024 * 1024):########0.00} GB", >= 1024 * 1024 => $"{fileSize / (1024 * 1024):####0.00} MB", >= 1024 => $"{fileSize / 1024:####0.00} KB", _ => $"{fileSize} bytes" }; } public static string FormatTime(int time, bool absolute = false) { var ts = TimeSpan.FromSeconds(time); var totalHours = (int)ts.TotalHours; var minutes = ts.Minutes; var seconds = ts.Seconds; if (absolute) { return $"{totalHours:D2}:{minutes:D2}:{seconds:D2}"; } return totalHours == 0 ? $"{minutes:D2}m{seconds:D2}s" : $"{totalHours}h{minutes:D2}m{seconds:D2}s"; } /// /// 通过avid检测是否为版权内容, 如果是的话返回ep:xx格式 /// /// /// private static async Task FixAvidAsync(string avid) { if (!avid.All(char.IsDigit)) return avid; string api = $"https://www.bilibili.com/video/av{avid}/"; string location = await GetWebLocationAsync(api); return location.Contains("/ep") ? $"ep:{EpRegex().Match(location).Groups[1].Value}" : avid; } private static string GetAidByBV(string bv) { // 能在本地就在本地 return Core.Util.BilibiliBvConverter.Decode(bv).ToString(); } private static async Task GetEpidBySSIdAsync(string ssid) { string api = $"https://api.bilibili.com/pugv/view/web/season?season_id={ssid}"; string json = await GetWebSourceAsync(api); using var jDoc = JsonDocument.Parse(json); string epId = jDoc.RootElement.GetProperty("data").GetProperty("episodes").EnumerateArray().First().GetProperty("id").ToString(); return epId; } private static async Task GetEpIdByBangumiSSIdAsync(string ssId) { string api = $"https://{Core.Config.EPHOST}/pgc/view/web/season?season_id={ssId}"; string json = await GetWebSourceAsync(api); using var jDoc = JsonDocument.Parse(json); string epId = jDoc.RootElement.GetProperty("result").GetProperty("episodes").EnumerateArray().First().GetProperty("id").ToString(); return epId; } private static async Task GetEpIdByMDAsync(string mdId) { string api = $"https://api.bilibili.com/pgc/review/user?media_id={mdId}"; string json = await GetWebSourceAsync(api); using var jDoc = JsonDocument.Parse(json); string epId = jDoc.RootElement.GetProperty("result").GetProperty("media").GetProperty("new_ep").GetProperty("id").ToString(); return epId; } /// /// 输入一堆已存在的文件, 合并到新文件 /// /// /// public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath) { if (!files.Any()) return; if (files.Length == 1) { FileInfo fi = new(files[0]); fi.MoveTo(outputFilePath, true); return; } if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); string[] inputFilePaths = files; using var outputStream = File.Create(outputFilePath); foreach (var inputFilePath in inputFilePaths) { if (inputFilePath == "") continue; using var inputStream = File.OpenRead(inputFilePath); // Buffer size can be passed as the second argument. inputStream.CopyTo(outputStream); //Console.WriteLine("The file {0} has been processed.", inputFilePath); } //Global.ExplorerFile(outputFilePath); } /// /// 寻找指定目录下指定后缀的文件的详细路径 如".txt" /// /// /// /// public static string[] GetFiles(string dir, string ext) { List al = []; StringBuilder sb = new(); DirectoryInfo d = new(dir); foreach (FileInfo fi in d.GetFiles()) { if (fi.Extension.ToUpper() == ext.ToUpper()) { al.Add(fi.FullName); } } string[] res = al.ToArray(); Array.Sort(res); //排序 return res; } private static readonly char[] InvalidChars = "34,60,62,124,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,58,42,63,92,47" .Split(',').Select(s => (char)byte.Parse(s)).ToArray(); public static string GetValidFileName(string input, string re = "_", bool filterSlash = false) { string title = input; foreach (char invalidChar in InvalidChars) { title = title.Replace(invalidChar.ToString(), re); } if (filterSlash) { title = title.Replace("/", re); title = title.Replace("\\", re); } return title; } /// /// 获取url字符串参数, 返回参数值字符串 /// /// 参数名称 /// url字符串 /// public static string GetQueryString(string name, string url) { Regex re = QueryRegex(); MatchCollection mc = re.Matches(url); foreach (Match m in mc.Cast()) { if (m.Result("$2").Equals(name)) { return m.Result("$3"); } } return ""; } //https://s1.hdslb.com/bfs/static/player/main/video.9efc0c61.js public static string GetSession(string buvid3) { //这个参数可以没有 所以此处就不写具体实现了 throw new NotImplementedException(); } public static string GetSign(string parms) { string toEncode = parms + "59b43e04ad6965f34319062b478f83dd"; return string.Concat(MD5.HashData(Encoding.UTF8.GetBytes(toEncode)).Select(i => i.ToString("x2"))); } public static string GetTimeStamp(bool bflag) { DateTimeOffset ts = DateTimeOffset.Now; return (bflag ? ts.ToUnixTimeSeconds() : ts.ToUnixTimeMilliseconds()).ToString(); } //https://stackoverflow.com/questions/1344221/how-can-i-generate-random-alphanumeric-strings private static readonly Random random = new(); public static string GetRandomString(int length) { const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789"; return new string(Enumerable.Repeat(chars, length) .Select(s => s[random.Next(s.Length)]).ToArray()); } //https://stackoverflow.com/a/45088333 public static string ToQueryString(NameValueCollection nameValueCollection) { NameValueCollection httpValueCollection = HttpUtility.ParseQueryString(string.Empty); httpValueCollection.Add(nameValueCollection); return httpValueCollection.ToString()!; } public static Dictionary ToDictionary(this NameValueCollection nameValueCollection) { var dict = new Dictionary(); foreach (var key in nameValueCollection.AllKeys) { dict[key!] = nameValueCollection[key]!; } return dict; } public static NameValueCollection GetTVLoginParms() { NameValueCollection sb = new(); DateTime now = DateTime.Now; string deviceId = GetRandomString(20); string buvid = GetRandomString(37); string fingerprint = $"{now:yyyyMMddHHmmssfff}{GetRandomString(45)}"; sb.Add("appkey", "4409e2ce8ffd12b8"); sb.Add("auth_code", ""); sb.Add("bili_local_id", deviceId); sb.Add("build", "102801"); sb.Add("buvid", buvid); sb.Add("channel", "master"); sb.Add("device", "OnePlus"); sb.Add($"device_id", deviceId); sb.Add("device_name", "OnePlus7TPro"); sb.Add("device_platform", "Android10OnePlusHD1910"); sb.Add($"fingerprint", fingerprint); sb.Add($"guid", buvid); sb.Add($"local_fingerprint", fingerprint); sb.Add($"local_id", buvid); sb.Add("mobi_app", "android_tv_yst"); sb.Add("networkstate", "wifi"); sb.Add("platform", "android"); sb.Add("sys_ver", "29"); sb.Add($"ts", GetTimeStamp(true)); sb.Add($"sign", GetSign(ToQueryString(sb))); return sb; } /// /// 检测ffmpeg是否识别杜比视界 /// /// public static bool CheckFFmpegDOVI() { try { var process = new Process { StartInfo = new ProcessStartInfo { FileName = BBDownMuxer.FFMPEG, Arguments = "-version", UseShellExecute = false, RedirectStandardError = true, RedirectStandardOutput = true, CreateNoWindow = true } }; process.Start(); string info = process.StandardOutput.ReadToEnd() + Environment.NewLine + process.StandardError.ReadToEnd(); process.WaitForExit(); var match = LibavutilRegex().Match(info); if (!match.Success) return false; if((Convert.ToInt32(match.Groups[1].Value)==57 && Convert.ToInt32(match.Groups[1].Value) >= 17) || Convert.ToInt32(match.Groups[1].Value) > 57) { return true; } } catch (Exception) { } return false; } /// /// 获取章节信息 /// /// /// /// public static async Task> FetchPointsAsync(string cid, string aid) { var ponints = new List(); try { string api = $"https://api.bilibili.com/x/player/wbi/v2?cid={cid}&aid={aid}"; string json = await GetWebSourceAsync(api); using var infoJson = JsonDocument.Parse(json); if (infoJson.RootElement.GetProperty("data").TryGetProperty("view_points", out JsonElement vPoint)) { foreach (var point in vPoint.EnumerateArray()) { ponints.Add(new ViewPoint() { title = point.GetProperty("content").GetString()!, start = int.Parse(point.GetProperty("from").ToString()), end = int.Parse(point.GetProperty("to").ToString()) }); } } } catch (Exception) { } return ponints; } /// /// 生成metadata文件, 用于ffmpeg混流章节信息 /// /// /// public static string GetFFmpegMetaString(List points) { StringBuilder sb = new(); sb.AppendLine(";FFMETADATA"); foreach (var p in points) { var time = 1000; //固定 1000 sb.AppendLine("[CHAPTER]"); sb.AppendLine($"TIMEBASE=1/{time}"); sb.AppendLine($"START={p.start * time}"); sb.AppendLine($"END={p.end * time}"); sb.AppendLine($"title={p.title}"); sb.AppendLine(); } return sb.ToString(); } /// /// 生成metadata文件, 用于mp4box混流章节信息 /// /// /// public static string GetMp4boxMetaString(List points) { StringBuilder sb = new(); foreach (var p in points) { sb.AppendLine($"{FormatTime(p.start, true)} {p.title}"); } return sb.ToString(); } public static string? FindExecutable(string name) { var fileExt = OperatingSystem.IsWindows() ? ".exe" : ""; var searchPath = new [] { Environment.CurrentDirectory, Program.APP_DIR }; var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? []; return searchPath.Concat(envPath).Select(p => Path.Combine(p, name + fileExt)).FirstOrDefault(File.Exists); } public static string RSubString(string sub) { sub = sub[(sub.LastIndexOf('/') + 1)..]; return sub[..sub.LastIndexOf('.')]; } private static string GetMixinKey(string orig) { byte[] mixinKeyEncTab = [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13 ]; var tmp = new StringBuilder(32); foreach (var index in mixinKeyEncTab) { tmp.Append(orig[index]); } return tmp.ToString(); } public static async Task CheckLogin(string cookie) { try { var api = "https://api.bilibili.com/x/web-interface/nav"; var source = await GetWebSourceAsync(api); var json = JsonDocument.Parse(source).RootElement; var is_login = json.GetProperty("data").GetProperty("isLogin").GetBoolean(); var wbi_img = json.GetProperty("data").GetProperty("wbi_img"); Core.Config.WBI = GetMixinKey(RSubString(wbi_img.GetProperty("img_url").GetString()) + RSubString(wbi_img.GetProperty("sub_url").GetString())); LogDebug("wbi: {0}", Core.Config.WBI); return is_login; } catch (Exception) { return false; } } [GeneratedRegex("av(\\d+)")] private static partial Regex AvRegex(); [GeneratedRegex("[Bb][Vv]1(\\w+)")] private static partial Regex BVRegex(); [GeneratedRegex("/ep(\\d+)")] private static partial Regex EpRegex(); [GeneratedRegex("/ss(\\d+)")] private static partial Regex SsRegex(); [GeneratedRegex(@"space\.bilibili\.com/(\d+)")] private static partial Regex UidRegex(); [GeneratedRegex(@"\.bilibili\.tv\/\w+\/play\/\d+\/(\d+)")] private static partial Regex GlobalEpRegex(); [GeneratedRegex("bangumi/media/(md\\d+)")] private static partial Regex BangumiMdRegex(); [GeneratedRegex(@"window.__INITIAL_STATE__=([\s\S].*?);\(function\(\)")] private static partial Regex StateRegex(); [GeneratedRegex("md(\\d+)")] private static partial Regex MdRegex(); [GeneratedRegex("(^|&)?(\\w+)=([^&]+)(&|$)?", RegexOptions.Compiled)] private static partial Regex QueryRegex(); [GeneratedRegex("libavutil\\s+(\\d+)\\. +(\\d+)\\.")] private static partial Regex LibavutilRegex(); } ================================================ FILE: BBDown/CommandLineInvoker.cs ================================================ using System; using System.CommandLine; using System.CommandLine.Binding; using System.CommandLine.Parsing; using System.Threading.Tasks; namespace BBDown; internal static class CommandLineInvoker { private static readonly Argument Url = new("url", description: "视频地址 或 av|bv|BV|ep|ss"); private static readonly Option UseTvApi = new(["--use-tv-api", "-tv"], "使用TV端解析模式"); private static readonly Option UseAppApi = new(["--use-app-api", "-app"], "使用APP端解析模式"); private static readonly Option UseIntlApi = new(["--use-intl-api", "-intl"], "使用国际版(东南亚视频)解析模式"); private static readonly Option UseMP4box = new(["--use-mp4box"], "使用MP4Box来混流"); private static readonly Option EncodingPriority = new(["--encoding-priority", "-e"], "视频及音频编码的选择优先级, 用逗号分割 例: \"hevc,av1,avc,flac,eac3,m4a\""); private static readonly Option DfnPriority = new(["--dfn-priority", "-q"], "画质优先级,用逗号分隔 例: \"8K 超高清, 1080P 高码率, HDR 真彩, 杜比视界\""); private static readonly Option OnlyShowInfo = new(["--only-show-info", "-info"], "仅解析而不进行下载"); private static readonly Option HideStreams = new(["--hide-streams", "-hs"], "不要显示所有可用音视频流"); private static readonly Option Interactive = new(["--interactive", "-ia"], "交互式选择清晰度"); private static readonly Option ShowAll = new(["--show-all"], "展示所有分P标题"); private static readonly Option UseAria2c = new(["--use-aria2c", "-aria2"], "调用aria2c进行下载(你需要自行准备好二进制可执行文件)"); private static readonly Option Aria2cArgs = new(["--aria2c-args"], "调用aria2c的附加参数(默认参数包含\"-x16 -s16 -j16 -k 5M\", 使用时注意字符串转义)"); private static readonly Option MultiThread = new(["--multi-thread", "-mt"], "使用多线程下载(默认开启)"); private static readonly Option SelectPage = new(["--select-page", "-p"], "选择指定分p或分p范围: (-p 8 或 -p 1,2 或 -p 3-5 或 -p ALL 或 -p LAST 或 -p 3,5,LATEST)"); private static readonly Option SimplyMux = new(["--simply-mux"], "精简混流,不增加描述、作者等信息"); private static readonly Option AudioOnly = new(["--audio-only"], "仅下载音频"); private static readonly Option VideoOnly = new(["--video-only"], "仅下载视频"); private static readonly Option DanmakuOnly = new(["--danmaku-only"], "仅下载弹幕"); private static readonly Option CoverOnly = new(["--cover-only"], "仅下载封面"); private static readonly Option SubOnly = new(["--sub-only"], "仅下载字幕"); private static readonly Option Debug = new(["--debug"], "输出调试日志"); private static readonly Option SkipMux = new(["--skip-mux"], "跳过混流步骤"); private static readonly Option SkipSubtitle = new(["--skip-subtitle"], "跳过字幕下载"); private static readonly Option SkipCover = new(["--skip-cover"], "跳过封面下载"); private static readonly Option ForceHttp = new(["--force-http"], "下载音视频时强制使用HTTP协议替换HTTPS(默认开启)"); private static readonly Option DownloadDanmaku = new(["--download-danmaku", "-dd"], "下载弹幕"); private static readonly Option DownloadDanmakuFormats = new(["--download-danmaku-formats", "-ddf"], $"指定需下载的弹幕格式, 用逗号分隔, 可选 {string.Join('/', BBDownDanmakuFormatInfo.AllFormatNames)}, 默认: \"{string.Join(',', BBDownDanmakuFormatInfo.AllFormatNames)}\""); private static readonly Option SkipAi = new(["--skip-ai"], description: "跳过AI字幕下载(默认开启)"); private static readonly Option VideoAscending = new(["--video-ascending"], "视频升序(最小体积优先)"); private static readonly Option AudioAscending = new(["--audio-ascending"], "音频升序(最小体积优先)"); private static readonly Option AllowPcdn = new(["--allow-pcdn"], "不替换PCDN域名, 仅在正常情况与--upos-host均无法下载时使用"); private static readonly Option Language = new(["--language"], "设置混流的音频语言(代码), 如chi, jpn等"); private static readonly Option UserAgent = new(["--user-agent", "-ua"], "指定user-agent, 否则使用随机user-agent"); private static readonly Option Cookie = new(["--cookie", "-c"], "设置字符串cookie用以下载网页接口的会员内容"); private static readonly Option AccessToken = new(["--access-token", "-token"], "设置access_token用以下载TV/APP接口的会员内容"); private static readonly Option WorkDir = new(["--work-dir"], "设置程序的工作目录"); private static readonly Option FFmpegPath = new(["--ffmpeg-path"], "设置ffmpeg的路径"); private static readonly Option Mp4boxPath = new(["--mp4box-path"], "设置mp4box的路径"); private static readonly Option Aria2cPath = new(["--aria2c-path"], "设置aria2c的路径"); private static readonly Option UposHost = new(["--upos-host"], "自定义upos服务器"); private static readonly Option ForceReplaceHost = new(["--force-replace-host"], "强制替换下载服务器host(默认开启)"); private static readonly Option SaveArchivesToFile = new(["--save-archives-to-file"], "将下载过的视频记录到本地文件中, 用于后续跳过下载同个视频"); private static readonly Option DelayPerPage = new(["--delay-per-page"], "设置下载合集分P之间的下载间隔时间(单位: 秒, 默认无间隔)"); private static readonly Option FilePattern = new(["--file-pattern", "-F"], $"使用内置变量自定义单P存储文件名:\r\n\r\n" + $": 视频主标题\r\n" + $": 视频分P序号\r\n" + $": 视频分P序号(前缀补零)\r\n" + $": 视频分P标题\r\n" + $": 视频BV号\r\n" + $": 视频aid\r\n" + $": 视频cid\r\n" + $": 视频清晰度\r\n" + $": 视频分辨率\r\n" + $": 视频帧率\r\n" + $": 视频编码\r\n" + $": 视频码率\r\n" + $": 音频编码\r\n" + $": 音频码率\r\n" + $": 上传者名称\r\n" + $": 上传者mid\r\n" + $": 收藏夹/番剧/合集发布时间\r\n" + $": 视频发布时间(分p视频发布时间与相同)\r\n" + $": API类型(TV/APP/INTL/WEB)\r\n\r\n" + $"默认为: {Program.SinglePageDefaultSavePath}\r\n"); private static readonly Option MultiFilePattern = new(["--multi-file-pattern", "-M"], $"使用内置变量自定义多P存储文件名:\r\n\r\n默认为: {Program.MultiPageDefaultSavePath}\r\n"); private static readonly Option Host = new(["--host"], "指定BiliPlus host(使用BiliPlus需要access_token, 不需要cookie, 解析服务器能够获取你账号的大部分权限!)"); private static readonly Option EpHost = new(["--ep-host"], "指定BiliPlus EP host(用于代理api.bilibili.com/pgc/view/web/season, 大部分解析服务器不支持代理该接口)"); private static readonly Option TvHost = new(["--tv-host"], "自定义tv端接口请求Host(用于代理api.snm0516.aisee.tv)"); private static readonly Option Area = new(["--area"], "(hk|tw|th) 使用BiliPlus时必选, 指定BiliPlus area"); private static readonly Option ConfigFile = new(["--config-file"], "读取指定的BBDown本地配置文件(默认为: BBDown.config)");//以下仅为兼容旧版本命令行, 不建议使用 private static readonly Option Aria2cProxy = new(["--aria2c-proxy"], "调用aria2c进行下载时的代理地址配置") { IsHidden = true }; private static readonly Option OnlyHevc = new(["--only-hevc", "-hevc"], "只下载hevc编码") { IsHidden = true }; private static readonly Option OnlyAvc = new(["--only-avc", "-avc"], "只下载avc编码") { IsHidden = true }; private static readonly Option OnlyAv1 = new(["--only-av1", "-av1"], "只下载av1编码") { IsHidden = true }; private static readonly Option AddDfnSubfix = new(["--add-dfn-subfix"], "为文件加入清晰度后缀, 如XXX[1080P 高码率]") { IsHidden = true }; private static readonly Option NoPaddingPageNum = new(["--no-padding-page-num"], "不给分P序号补零") { IsHidden = true }; private static readonly Option BandwithAscending = new(["--bandwith-ascending"], "比特率升序(最小体积优先)") { IsHidden = true }; class MyOptionBinder : BinderBase { protected override MyOption GetBoundValue(BindingContext bindingContext) { var option = new MyOption { Url = bindingContext.ParseResult.GetValueForArgument(Url) }; if (bindingContext.ParseResult.HasOption(UseTvApi)) option.UseTvApi = bindingContext.ParseResult.GetValueForOption(UseTvApi)!; if (bindingContext.ParseResult.HasOption(UseAppApi)) option.UseAppApi = bindingContext.ParseResult.GetValueForOption(UseAppApi)!; if (bindingContext.ParseResult.HasOption(UseIntlApi)) option.UseIntlApi = bindingContext.ParseResult.GetValueForOption(UseIntlApi)!; if (bindingContext.ParseResult.HasOption(UseMP4box)) option.UseMP4box = bindingContext.ParseResult.GetValueForOption(UseMP4box)!; if (bindingContext.ParseResult.HasOption(EncodingPriority)) option.EncodingPriority = bindingContext.ParseResult.GetValueForOption(EncodingPriority)!; if (bindingContext.ParseResult.HasOption(DfnPriority)) option.DfnPriority = bindingContext.ParseResult.GetValueForOption(DfnPriority)!; if (bindingContext.ParseResult.HasOption(OnlyShowInfo)) option.OnlyShowInfo = bindingContext.ParseResult.GetValueForOption(OnlyShowInfo)!; if (bindingContext.ParseResult.HasOption(ShowAll)) option.ShowAll = bindingContext.ParseResult.GetValueForOption(ShowAll)!; if (bindingContext.ParseResult.HasOption(UseAria2c)) option.UseAria2c = bindingContext.ParseResult.GetValueForOption(UseAria2c)!; if (bindingContext.ParseResult.HasOption(Interactive)) option.Interactive = bindingContext.ParseResult.GetValueForOption(Interactive)!; if (bindingContext.ParseResult.HasOption(HideStreams)) option.HideStreams = bindingContext.ParseResult.GetValueForOption(HideStreams)!; if (bindingContext.ParseResult.HasOption(MultiThread)) option.MultiThread = bindingContext.ParseResult.GetValueForOption(MultiThread)!; if (bindingContext.ParseResult.HasOption(SimplyMux)) option.SimplyMux = bindingContext.ParseResult.GetValueForOption(SimplyMux)!; if (bindingContext.ParseResult.HasOption(VideoOnly)) option.VideoOnly = bindingContext.ParseResult.GetValueForOption(VideoOnly)!; if (bindingContext.ParseResult.HasOption(AudioOnly)) option.AudioOnly = bindingContext.ParseResult.GetValueForOption(AudioOnly)!; if (bindingContext.ParseResult.HasOption(DanmakuOnly)) option.DanmakuOnly = bindingContext.ParseResult.GetValueForOption(DanmakuOnly)!; if (bindingContext.ParseResult.HasOption(CoverOnly)) option.CoverOnly = bindingContext.ParseResult.GetValueForOption(CoverOnly)!; if (bindingContext.ParseResult.HasOption(SubOnly)) option.SubOnly = bindingContext.ParseResult.GetValueForOption(SubOnly)!; if (bindingContext.ParseResult.HasOption(Debug)) option.Debug = bindingContext.ParseResult.GetValueForOption(Debug)!; if (bindingContext.ParseResult.HasOption(SkipMux)) option.SkipMux = bindingContext.ParseResult.GetValueForOption(SkipMux)!; if (bindingContext.ParseResult.HasOption(SkipSubtitle)) option.SkipSubtitle = bindingContext.ParseResult.GetValueForOption(SkipSubtitle)!; if (bindingContext.ParseResult.HasOption(SkipCover)) option.SkipCover = bindingContext.ParseResult.GetValueForOption(SkipCover)!; if (bindingContext.ParseResult.HasOption(ForceHttp)) option.ForceHttp = bindingContext.ParseResult.GetValueForOption(ForceHttp)!; if (bindingContext.ParseResult.HasOption(DownloadDanmaku)) option.DownloadDanmaku = bindingContext.ParseResult.GetValueForOption(DownloadDanmaku)!; if (bindingContext.ParseResult.HasOption(DownloadDanmakuFormats)) option.DownloadDanmakuFormats = bindingContext.ParseResult.GetValueForOption(DownloadDanmakuFormats)!; if (bindingContext.ParseResult.HasOption(SkipAi)) option.SkipAi = bindingContext.ParseResult.GetValueForOption(SkipAi)!; if (bindingContext.ParseResult.HasOption(VideoAscending)) option.VideoAscending = bindingContext.ParseResult.GetValueForOption(VideoAscending)!; if (bindingContext.ParseResult.HasOption(AudioAscending)) option.AudioAscending = bindingContext.ParseResult.GetValueForOption(AudioAscending)!; if (bindingContext.ParseResult.HasOption(AllowPcdn)) option.AllowPcdn = bindingContext.ParseResult.GetValueForOption(AllowPcdn)!; if (bindingContext.ParseResult.HasOption(FilePattern)) option.FilePattern = bindingContext.ParseResult.GetValueForOption(FilePattern)!; if (bindingContext.ParseResult.HasOption(MultiFilePattern)) option.MultiFilePattern = bindingContext.ParseResult.GetValueForOption(MultiFilePattern)!; if (bindingContext.ParseResult.HasOption(SelectPage)) option.SelectPage = bindingContext.ParseResult.GetValueForOption(SelectPage)!; if (bindingContext.ParseResult.HasOption(Language)) option.Language = bindingContext.ParseResult.GetValueForOption(Language)!; if (bindingContext.ParseResult.HasOption(UserAgent)) option.UserAgent = bindingContext.ParseResult.GetValueForOption(UserAgent)!; if (bindingContext.ParseResult.HasOption(Cookie)) option.Cookie = bindingContext.ParseResult.GetValueForOption(Cookie)!; if (bindingContext.ParseResult.HasOption(AccessToken)) option.AccessToken = bindingContext.ParseResult.GetValueForOption(AccessToken)!; if (bindingContext.ParseResult.HasOption(Aria2cArgs)) option.Aria2cArgs = bindingContext.ParseResult.GetValueForOption(Aria2cArgs)!; if (bindingContext.ParseResult.HasOption(WorkDir)) option.WorkDir = bindingContext.ParseResult.GetValueForOption(WorkDir)!; if (bindingContext.ParseResult.HasOption(FFmpegPath)) option.FFmpegPath = bindingContext.ParseResult.GetValueForOption(FFmpegPath)!; if (bindingContext.ParseResult.HasOption(Mp4boxPath)) option.Mp4boxPath = bindingContext.ParseResult.GetValueForOption(Mp4boxPath)!; if (bindingContext.ParseResult.HasOption(Aria2cPath)) option.Aria2cPath = bindingContext.ParseResult.GetValueForOption(Aria2cPath)!; if (bindingContext.ParseResult.HasOption(UposHost)) option.UposHost = bindingContext.ParseResult.GetValueForOption(UposHost)!; if (bindingContext.ParseResult.HasOption(ForceReplaceHost)) option.ForceReplaceHost = bindingContext.ParseResult.GetValueForOption(ForceReplaceHost)!; if (bindingContext.ParseResult.HasOption(SaveArchivesToFile)) option.SaveArchivesToFile = bindingContext.ParseResult.GetValueForOption(SaveArchivesToFile)!; if (bindingContext.ParseResult.HasOption(DelayPerPage)) option.DelayPerPage = bindingContext.ParseResult.GetValueForOption(DelayPerPage)!; if (bindingContext.ParseResult.HasOption(Host)) option.Host = bindingContext.ParseResult.GetValueForOption(Host)!; if (bindingContext.ParseResult.HasOption(EpHost)) option.EpHost = bindingContext.ParseResult.GetValueForOption(EpHost)!; if (bindingContext.ParseResult.HasOption(TvHost)) option.TvHost = bindingContext.ParseResult.GetValueForOption(TvHost)!; if (bindingContext.ParseResult.HasOption(Area)) option.Area = bindingContext.ParseResult.GetValueForOption(Area)!; if (bindingContext.ParseResult.HasOption(ConfigFile)) option.ConfigFile = bindingContext.ParseResult.GetValueForOption(ConfigFile)!; if (bindingContext.ParseResult.HasOption(Aria2cProxy)) option.Aria2cProxy = bindingContext.ParseResult.GetValueForOption(Aria2cProxy)!; if (bindingContext.ParseResult.HasOption(OnlyHevc)) option.OnlyHevc = bindingContext.ParseResult.GetValueForOption(OnlyHevc)!; if (bindingContext.ParseResult.HasOption(OnlyAvc)) option.OnlyAvc = bindingContext.ParseResult.GetValueForOption(OnlyAvc)!; if (bindingContext.ParseResult.HasOption(OnlyAv1)) option.OnlyAv1 = bindingContext.ParseResult.GetValueForOption(OnlyAv1)!; if (bindingContext.ParseResult.HasOption(AddDfnSubfix)) option.AddDfnSubfix = bindingContext.ParseResult.GetValueForOption(AddDfnSubfix)!; if (bindingContext.ParseResult.HasOption(NoPaddingPageNum)) option.NoPaddingPageNum = bindingContext.ParseResult.GetValueForOption(NoPaddingPageNum)!; if (bindingContext.ParseResult.HasOption(BandwithAscending)) option.BandwithAscending = bindingContext.ParseResult.GetValueForOption(BandwithAscending)!; return option; } } public static RootCommand GetRootCommand(Func action) { var rootCommand = new RootCommand { Url, UseTvApi, UseAppApi, UseIntlApi, UseMP4box, EncodingPriority, DfnPriority, OnlyShowInfo, ShowAll, UseAria2c, Interactive, HideStreams, MultiThread, VideoOnly, AudioOnly, DanmakuOnly, SubOnly, CoverOnly, Debug, SkipMux, SkipSubtitle, SkipCover, ForceHttp, DownloadDanmaku, DownloadDanmakuFormats, SkipAi, VideoAscending, AudioAscending, AllowPcdn, FilePattern, MultiFilePattern, SelectPage, Language, UserAgent, Cookie, AccessToken, Aria2cArgs, WorkDir, FFmpegPath, Mp4boxPath, Aria2cPath, UposHost, ForceReplaceHost, SaveArchivesToFile, DelayPerPage, Host, EpHost, TvHost, Area, ConfigFile, Aria2cProxy, OnlyHevc, OnlyAvc, OnlyAv1, AddDfnSubfix, NoPaddingPageNum, BandwithAscending }; rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); return rootCommand; } } ================================================ FILE: BBDown/ConsoleQRCode.cs ================================================ using QRCoder; using System; namespace BBDown; public class ConsoleQRCode : AbstractQRCode { public ConsoleQRCode() { } public ConsoleQRCode(QRCodeData data) : base(data) { } public void GetGraphic() => GetGraphic(ConsoleColor.Black, ConsoleColor.White); public void GetGraphic(ConsoleColor darkColor, ConsoleColor lightColor) { var previousBackColor = Console.BackgroundColor; var previousForeColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.White; for (int y = 0; y < QrCodeData.ModuleMatrix.Count; y++) { for (int x = 0; x < QrCodeData.ModuleMatrix[y].Count; x++) { Console.ForegroundColor = QrCodeData.ModuleMatrix[y][x] ? darkColor : lightColor; Console.Write("██"); } Console.BackgroundColor = darkColor; Console.WriteLine(""); } Console.BackgroundColor = previousBackColor; Console.ForegroundColor = previousForeColor; } } ================================================ FILE: BBDown/Directory.Build.props ================================================ Speed true true false true true true aarch64-linux-gnu-objcopy ================================================ FILE: BBDown/Model/ServeRequestOptions.cs ================================================ using BBDown; internal class ServeRequestOptions : MyOption { /// /// 任务完成回调Http请求地址 /// public string? CallBackWebHook { get; set; } } ================================================ FILE: BBDown/MyOption.cs ================================================ namespace BBDown; internal class MyOption { public string Url { get; set; } = default!; public bool UseTvApi { get; set; } public bool UseAppApi { get; set; } public bool UseIntlApi { get; set; } public bool UseMP4box { get; set; } public string? EncodingPriority { get; set; } public string? DfnPriority { get; set; } public bool OnlyShowInfo { get; set; } public bool ShowAll { get; set; } public bool UseAria2c { get; set; } public bool Interactive { get; set; } public bool HideStreams { get; set; } public bool MultiThread { get; set; } = true; public bool SimplyMux { get; set; } = false; public bool VideoOnly { get; set; } public bool AudioOnly { get; set; } public bool DanmakuOnly { get; set; } public bool CoverOnly { get; set; } public bool SubOnly { get; set; } public bool Debug { get; set; } public bool SkipMux { get; set; } public bool SkipSubtitle { get; set; } public bool SkipCover { get; set; } public bool ForceHttp { get; set; } = true; public bool DownloadDanmaku { get; set; } = false; public string? DownloadDanmakuFormats { get; set; } public bool SkipAi { get; set; } = true; public bool VideoAscending { get; set; } = false; public bool AudioAscending { get; set; } = false; public bool AllowPcdn { get; set; } = false; public bool ForceReplaceHost { get; set; } = true; public bool SaveArchivesToFile { get; set; } = false; public string FilePattern { get; set; } = ""; public string MultiFilePattern { get; set; } = ""; public string SelectPage { get; set; } = ""; public string Language { get; set; } = ""; public string UserAgent { get; set; } = ""; public string Cookie { get; set; } = ""; public string AccessToken { get; set; } = ""; public string Aria2cArgs { get; set; } = ""; public string WorkDir { get; set; } = ""; public string FFmpegPath { get; set; } = ""; public string Mp4boxPath { get; set; } = ""; public string Aria2cPath { get; set; } = ""; public string UposHost { get; set; } = ""; public string DelayPerPage { get; set; } = "0"; public string Host { get; set; } = "api.bilibili.com"; public string EpHost { get; set; } = "api.bilibili.com"; public string TvHost { get; set; } = "api.snm0516.aisee.tv"; public string Area { get; set; } = ""; public string? ConfigFile { get; set; } //以下仅为兼容旧版本命令行,不建议使用 public string Aria2cProxy { get; set; } = ""; public bool OnlyHevc { get; set; } public bool OnlyAvc { get; set; } public bool OnlyAv1 { get; set; } public bool AddDfnSubfix { get; set; } public bool NoPaddingPageNum { get; set; } public bool BandwithAscending { get; set; } } ================================================ FILE: BBDown/Program.Methods.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using static BBDown.Core.Entity.Entity; using static BBDown.BBDownUtil; using static BBDown.Core.Logger; using System.Linq; using System.Text.RegularExpressions; using BBDown.Core; using BBDown.Core.Entity; using static BBDown.BBDownDownloadUtil; namespace BBDown; internal partial class Program { /// /// 兼容旧版本命令行参数并给出警告 /// /// private static void HandleDeprecatedOptions(MyOption myOption) { if (myOption.AddDfnSubfix) { LogWarn("--add-dfn-subfix 已被弃用, 建议使用 --file-pattern/-F 或 --multi-file-pattern/-M 来自定义输出文件名格式"); if (string.IsNullOrEmpty(myOption.FilePattern) && string.IsNullOrEmpty(myOption.MultiFilePattern)) { SinglePageDefaultSavePath += "[]"; MultiPageDefaultSavePath += "[]"; LogWarn($"已切换至 -F \"{SinglePageDefaultSavePath}\" -M \"{MultiPageDefaultSavePath}\""); } } if (myOption.Aria2cProxy != "") { LogWarn("--aria2c-proxy 已被弃用, 请使用 --aria2c-args 来设置aria2c代理, 本次执行已添加该代理"); myOption.Aria2cArgs += $" --all-proxy=\"{myOption.Aria2cProxy}\""; } if (myOption.OnlyHevc) { LogWarn("--only-hevc/-hevc 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将hevc设置为最高优先级"); myOption.EncodingPriority = "hevc"; } if (myOption.OnlyAvc) { LogWarn("--only-avc/-avc 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将avc设置为最高优先级"); myOption.EncodingPriority = "avc"; } if (myOption.OnlyAv1) { LogWarn("--only-av1/-av1 已被弃用, 请使用 --encoding-priority 来设置编码优先级, 本次执行已将av1设置为最高优先级"); myOption.EncodingPriority = "av1"; } if (myOption.NoPaddingPageNum) { LogWarn("--no-padding-page-num 已被弃用, 建议使用 --file-pattern/-F 或 --multi-file-pattern/-M 来自定义输出文件名格式"); if (string.IsNullOrEmpty(myOption.FilePattern) && string.IsNullOrEmpty(myOption.MultiFilePattern)) { MultiPageDefaultSavePath = MultiPageDefaultSavePath.Replace("", ""); LogWarn($"已切换至 -M \"{MultiPageDefaultSavePath}\""); } } if (myOption.BandwithAscending) { LogWarn("--bandwith-ascending 已被弃用, 建议使用 --video-ascending 与 --audio-ascending 来指定视频或音频是否升序, 本次执行已将视频与音频均设为升序"); myOption.VideoAscending = true; myOption.AudioAscending = true; } } /// /// 解析用户指定的编码优先级 /// /// /// private static Dictionary ParseEncodingPriority(MyOption myOption, out string firstEncoding) { var encodingPriority = new Dictionary(); firstEncoding = ""; if (myOption.EncodingPriority != null) { var encodingPriorityTemp = myOption.EncodingPriority .ToUpper() .Replace(',', ',') .Replace("-", string.Empty) .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(s => !string.IsNullOrEmpty(s)).ToList(); byte index = 0; firstEncoding = encodingPriorityTemp.First(); foreach (string encoding in encodingPriorityTemp) { if (encodingPriority.ContainsKey(encoding)) continue; encodingPriority[encoding] = index; index++; } } return encodingPriority; } private static BBDownDanmakuFormat[] ParseDownloadDanmakuFormats(MyOption myOption) { if (string.IsNullOrEmpty(myOption.DownloadDanmakuFormats)) return BBDownDanmakuFormatInfo.DefaultFormats; var formats = myOption.DownloadDanmakuFormats.Replace(",", ",").ToLower().Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (formats.Any(format => !BBDownDanmakuFormatInfo.AllFormatNames.Contains(format))) { LogError($"包含不支持的下载弹幕格式:{myOption.DownloadDanmakuFormats}"); return BBDownDanmakuFormatInfo.DefaultFormats; } return formats.Select(BBDownDanmakuFormatInfo.FromFormatName).ToArray(); } /// /// 解析用户输入的清晰度规格优先级 /// /// /// private static Dictionary ParseDfnPriority(MyOption myOption) { var dfnPriority = new Dictionary(); if (myOption.DfnPriority != null) { var dfnPriorityTemp = myOption.DfnPriority.Replace(",", ",").Split(',').Select(s => s.ToUpper().Trim()).Where(s => !string.IsNullOrEmpty(s)); int index = 0; foreach (string dfn in dfnPriorityTemp) { if (dfnPriority.ContainsKey(dfn)) { continue; } dfnPriority[dfn] = index; index++; } } return dfnPriority; } /// /// 寻找并设置所需的二进制文件 /// /// /// private static void FindBinaries(MyOption myOption) { if (!string.IsNullOrEmpty(myOption.FFmpegPath) && File.Exists(myOption.FFmpegPath)) { BBDownMuxer.FFMPEG = myOption.FFmpegPath; } if (!string.IsNullOrEmpty(myOption.Mp4boxPath) && File.Exists(myOption.Mp4boxPath)) { BBDownMuxer.MP4BOX = myOption.Mp4boxPath; } if (!string.IsNullOrEmpty(myOption.Aria2cPath) && File.Exists(myOption.Aria2cPath)) { BBDownAria2c.ARIA2C = myOption.Aria2cPath; } //寻找ffmpeg或mp4box if (!myOption.SkipMux) { if (myOption.UseMP4box) { if (string.IsNullOrEmpty(BBDownMuxer.MP4BOX) || !File.Exists(BBDownMuxer.MP4BOX)) { var binPath = FindExecutable("mp4box") ?? FindExecutable("MP4box"); if (string.IsNullOrEmpty(binPath)) throw new Exception("找不到可执行的mp4box文件"); BBDownMuxer.MP4BOX = binPath; } } else if (string.IsNullOrEmpty(BBDownMuxer.FFMPEG) || !File.Exists(BBDownMuxer.FFMPEG)) { var binPath = FindExecutable("ffmpeg"); if (string.IsNullOrEmpty(binPath)) throw new Exception("找不到可执行的ffmpeg文件"); BBDownMuxer.FFMPEG = binPath; } } //寻找aria2c if (myOption.UseAria2c) { if (string.IsNullOrEmpty(BBDownAria2c.ARIA2C) || !File.Exists(BBDownAria2c.ARIA2C)) { var binPath = FindExecutable("aria2c"); if (string.IsNullOrEmpty(binPath)) throw new Exception("找不到可执行的aria2c文件"); BBDownAria2c.ARIA2C = binPath; } } } /// /// 处理有冲突的选项 /// /// private static void HandleConflictingOptions(MyOption myOption) { //手动选择时不能隐藏流 if (myOption.Interactive) { myOption.HideStreams = false; } //audioOnly和videoOnly同时开启则全部忽视 if (myOption.AudioOnly && myOption.VideoOnly) { myOption.AudioOnly = false; myOption.VideoOnly = false; } if (myOption.SkipSubtitle) { myOption.SubOnly = false; } } /// /// 设置用户输入的自定义工作目录 /// /// private static void ChangeWorkingDir(MyOption myOption) { if (!string.IsNullOrEmpty(myOption.WorkDir)) { //解释环境变量 myOption.WorkDir = Environment.ExpandEnvironmentVariables(myOption.WorkDir); var dir = Path.GetFullPath(myOption.WorkDir); if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } //设置工作目录 Environment.CurrentDirectory = dir; LogDebug("切换工作目录至:{0}", dir); } } /// /// 加载用户的认证信息(cookie或token) /// /// private static void LoadCredentials(MyOption myOption) { if (string.IsNullOrEmpty(Config.COOKIE) && File.Exists(Path.Combine(APP_DIR, "BBDown.data"))) { Log("加载本地cookie..."); LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDown.data")); Config.COOKIE = File.ReadAllText(Path.Combine(APP_DIR, "BBDown.data")); } if (string.IsNullOrEmpty(Config.TOKEN) && File.Exists(Path.Combine(APP_DIR, "BBDownTV.data")) && myOption.UseTvApi) { Log("加载本地token..."); LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDownTV.data")); Config.TOKEN = File.ReadAllText(Path.Combine(APP_DIR, "BBDownTV.data")); Config.TOKEN = Config.TOKEN.Replace("access_token=", ""); } if (string.IsNullOrEmpty(Config.TOKEN) && File.Exists(Path.Combine(APP_DIR, "BBDownApp.data")) && myOption.UseAppApi) { Log("加载本地token..."); LogDebug("文件路径:{0}", Path.Combine(APP_DIR, "BBDownApp.data")); Config.TOKEN = File.ReadAllText(Path.Combine(APP_DIR, "BBDownApp.data")); Config.TOKEN = Config.TOKEN.Replace("access_token=", ""); } } private static object fileLock = new object(); public static void SaveAidToFile(string aid) { lock (fileLock) { string filePath = Path.Combine(APP_DIR, "BBDown.archives"); LogDebug("文件路径:{0}", filePath); File.AppendAllText(filePath, $"{aid}|"); } } public static bool CheckAidFromFile(string aid) { lock (fileLock) { string filePath = Path.Combine(APP_DIR, "BBDown.archives"); if (!File.Exists(filePath)) return false; LogDebug("文件路径:{0}", filePath); var text = File.ReadAllText(filePath); return text.Split('|').Any(item => item == aid); } } /// /// 获取选中的分P列表 /// /// /// /// /// private static List? GetSelectedPages(MyOption myOption, VInfo vInfo, string input) { List? selectedPages = null; List pagesInfo = vInfo.PagesInfo; string selectPage = myOption.SelectPage.ToUpper().Trim().Trim(','); if (string.IsNullOrEmpty(selectPage)) { //如果用户没有选择分P, 根据epid或query param来确定某一集 if (!string.IsNullOrEmpty(vInfo.Index)) { selectedPages = [vInfo.Index]; Log("程序已自动选择你输入的集数, 如果要下载其他集数请自行指定分P(如可使用-p ALL代表全部)"); } else if (!string.IsNullOrEmpty(GetQueryString("p", input))) { selectedPages = [GetQueryString("p", input)]; Log("程序已自动选择你输入的集数, 如果要下载其他集数请自行指定分P(如可使用-p ALL代表全部)"); } } else if (selectPage != "ALL") { selectedPages = new List(); //选择最新分P string lastPage = pagesInfo.Count.ToString(); foreach (string key in new[] { "LAST", "NEW", "LATEST" }) { selectPage = selectPage.Replace(key, lastPage); } try { if (selectPage.Contains('-')) { string[] tmp = selectPage.Split('-'); int start = int.Parse(tmp[0]); int end = int.Parse(tmp[1]); for (int i = start; i <= end; i++) { selectedPages.Add(i.ToString()); } } else { foreach (var s in selectPage.Split(',')) { selectedPages.Add(s); } } } catch { LogError("解析分P参数时失败了~"); selectedPages = null; }; } return selectedPages; } /// /// 处理CDN域名 /// /// /// /// private static void HandlePcdn(MyOption myOption, Video? selectedVideo, Audio? selectedAudio) { if (myOption.UposHost == "") { //处理PCDN if (!myOption.AllowPcdn) { var pcdnReg = PcdnRegex(); if (selectedVideo != null && pcdnReg.IsMatch(selectedVideo.baseUrl)) { LogWarn($"检测到视频流为PCDN, 尝试强制替换为{BACKUP_HOST}……"); selectedVideo.baseUrl = pcdnReg.Replace(selectedVideo.baseUrl, $"://{BACKUP_HOST}/"); } if (selectedAudio != null && pcdnReg.IsMatch(selectedAudio.baseUrl)) { LogWarn($"检测到音频流为PCDN, 尝试强制替换为{BACKUP_HOST}……"); selectedAudio.baseUrl = pcdnReg.Replace(selectedAudio.baseUrl, $"://{BACKUP_HOST}/"); } } var akamReg = AkamRegex(); if (selectedVideo != null && Config.AREA != "" && selectedVideo.baseUrl.Contains("akamaized.net")) { LogWarn($"检测到视频流为外国源, 尝试强制替换为{BACKUP_HOST}……"); selectedVideo.baseUrl = akamReg.Replace(selectedVideo.baseUrl, $"://{BACKUP_HOST}/"); } if (selectedAudio != null && Config.AREA != "" && selectedAudio.baseUrl.Contains("akamaized.net")) { LogWarn($"检测到音频流为外国源, 尝试强制替换为{BACKUP_HOST}……"); selectedAudio.baseUrl = akamReg.Replace(selectedAudio.baseUrl, $"://{BACKUP_HOST}/"); } } else { if (selectedVideo != null) { LogWarn($"尝试将视频流强制替换为{myOption.UposHost}……"); selectedVideo.baseUrl = UposRegex().Replace(selectedVideo.baseUrl, $"://{myOption.UposHost}/"); } if (selectedAudio != null) { LogWarn($"尝试将音频流强制替换为{myOption.UposHost}……"); selectedAudio.baseUrl = UposRegex().Replace(selectedAudio.baseUrl, $"://{myOption.UposHost}/"); } } } /// /// 打印解析到的各个轨道信息 /// /// /// private static void PrintAllTracksInfo(ParsedResult parsedResult, int pageDur, bool onlyShowInfo) { if (parsedResult.BackgroundAudioTracks.Any() && parsedResult.RoleAudioList.Any()) { Log($"共计{parsedResult.BackgroundAudioTracks.Count}条背景音频流."); int index = 0; foreach (var a in parsedResult.BackgroundAudioTracks) { int pDur = pageDur == 0 ? a.dur : pageDur; LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false); } Log($"共计{parsedResult.RoleAudioList.Count}条配音, 每条包含{parsedResult.RoleAudioList[0].audio.Count}条配音流."); index = 0; foreach (var a in parsedResult.RoleAudioList[0].audio) { int pDur = pageDur == 0 ? a.dur : pageDur; LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false); } } //展示所有的音视频流信息 if (parsedResult.VideoTracks.Any()) { Log($"共计{parsedResult.VideoTracks.Count}条视频流."); int index = 0; foreach (var v in parsedResult.VideoTracks) { int pDur = pageDur == 0 ? v.dur : pageDur; var size = v.size > 0 ? v.size : pDur * v.bandwith * 1024 / 8; LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [{v.bandwith} kbps] [~{FormatFileSize(size)}]".Replace("[] ", ""), false); if (onlyShowInfo) Console.WriteLine(v.baseUrl); } } if (parsedResult.AudioTracks.Any()) { Log($"共计{parsedResult.AudioTracks.Count}条音频流."); int index = 0; foreach (var a in parsedResult.AudioTracks) { int pDur = pageDur == 0 ? a.dur : pageDur; LogColor($"{index++}. [{a.codecs}] [{a.bandwith} kbps] [~{FormatFileSize(pDur * a.bandwith * 1024 / 8)}]", false); if (onlyShowInfo) Console.WriteLine(a.baseUrl); } } } private static void PrintSelectedTrackInfo(Video? selectedVideo, Audio? selectedAudio, int pageDur) { if (selectedVideo != null) { int pDur = pageDur == 0 ? selectedVideo.dur : pageDur; var size = selectedVideo.size > 0 ? selectedVideo.size : pDur * selectedVideo.bandwith * 1024 / 8; LogColor($"[视频] [{selectedVideo.dfn}] [{selectedVideo.res}] [{selectedVideo.codecs}] [{selectedVideo.fps}] [{selectedVideo.bandwith} kbps] [~{FormatFileSize(size)}]".Replace("[] ", ""), false); } if (selectedAudio != null) { int pDur = pageDur == 0 ? selectedAudio.dur : pageDur; LogColor($"[音频] [{selectedAudio.codecs}] [{selectedAudio.bandwith} kbps] [~{FormatFileSize(pDur * selectedAudio.bandwith * 1024 / 8)}]", false); } } /// /// 引导用户进行手动选择轨道 /// /// /// /// private static void SelectTrackManually(ParsedResult parsedResult, ref int vIndex, ref int aIndex) { if (parsedResult.VideoTracks.Any()) { Log("请选择一条视频流(输入序号): ", false); Console.ForegroundColor = ConsoleColor.Cyan; vIndex = Convert.ToInt32(Console.ReadLine()); if (vIndex > parsedResult.VideoTracks.Count || vIndex < 0) vIndex = 0; Console.ResetColor(); } if (parsedResult.AudioTracks.Any()) { Log("请选择一条音频流(输入序号): ", false); Console.ForegroundColor = ConsoleColor.Cyan; aIndex = Convert.ToInt32(Console.ReadLine()); if (aIndex > parsedResult.AudioTracks.Count || aIndex < 0) aIndex = 0; Console.ResetColor(); } } /// /// 下载轨道 /// /// private static async Task DownloadTrackAsync(string url, string destPath, DownloadConfig downloadConfig, bool video) { if (downloadConfig.MultiThread && !url.Contains("-cmcc-")) { await MultiThreadDownloadFileAsync(url, destPath, downloadConfig); Log($"合并{(video ? "视频" : "音频")}分片..."); CombineMultipleFilesIntoSingleFile(GetFiles(Path.GetDirectoryName(destPath)!, $".{(video ? "v" : "a")}clip"), destPath); Log("清理分片..."); foreach (var file in new DirectoryInfo(Path.GetDirectoryName(destPath)!).EnumerateFiles("*.?clip")) file.Delete(); } else { if (downloadConfig.MultiThread && url.Contains("-cmcc-")) { LogWarn("检测到cmcc域名cdn, 已经禁用多线程"); downloadConfig.ForceHttp = false; } await DownloadFileAsync(url, destPath, downloadConfig); } } [GeneratedRegex("://.*:\\d+/")] private static partial Regex PcdnRegex(); [GeneratedRegex("://.*akamaized\\.net/")] private static partial Regex AkamRegex(); [GeneratedRegex("://[^/]+/")] private static partial Regex UposRegex(); } ================================================ FILE: BBDown/Program.cs ================================================ using System; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Parsing; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using static BBDown.Core.Entity.Entity; using static BBDown.BBDownUtil; using static BBDown.BBDownDownloadUtil; using static BBDown.Core.Parser; using static BBDown.Core.Logger; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using BBDown.Core; using BBDown.Core.Util; using System.Text.Json.Serialization; using System.CommandLine.Builder; using BBDown.Core.Entity; namespace BBDown; partial class Program { private static readonly string BACKUP_HOST = "upos-sz-mirrorcoso1.bilivideo.com"; public static string SinglePageDefaultSavePath { get; set; } = ""; public static string MultiPageDefaultSavePath { get; set; } = "/[P]"; public static readonly string APP_DIR = Path.GetDirectoryName(Environment.ProcessPath)!; private static string FormatTimeStamp(long ts, string format) { try { return ts == 0 ? "null" : DateTimeOffset.FromUnixTimeSeconds(ts).ToLocalTime().ToString(format); } catch (Exception ex) { LogError($"格式化日期出错: {ex.Message}"); return ts.ToString(); } } [JsonSerializable(typeof(MyOption))] [JsonSerializable(typeof(ServeRequestOptions))] partial class MyOptionJsonContext : JsonSerializerContext { } private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) { LogWarn("Force Exit..."); try { Console.ResetColor(); Console.CursorVisible = true; if (!OperatingSystem.IsWindows()) System.Diagnostics.Process.Start("stty", "echo"); } catch { } Environment.Exit(0); } public static async Task Main(params string[] args) { Console.CancelKeyPress += Console_CancelKeyPress; ServicePointManager.DefaultConnectionLimit = 2048; var rootCommand = CommandLineInvoker.GetRootCommand(RunApp); Command loginCommand = new( "login", "通过APP扫描二维码以登录您的WEB账号"); rootCommand.AddCommand(loginCommand); Command loginTVCommand = new( "logintv", "通过APP扫描二维码以登录您的TV账号"); rootCommand.AddCommand(loginTVCommand); var serverUrlOpt = new Option( ["--listen", "-l"], description: "服务器监听url"); Command runAsServerCommand = new( "serve", "以服务器模式运行") { serverUrlOpt }; runAsServerCommand.SetHandler(StartServer, serverUrlOpt); rootCommand.AddCommand(runAsServerCommand); rootCommand.Description = "BBDown是一个免费且便捷高效的哔哩哔哩下载/解析软件."; rootCommand.TreatUnmatchedTokensAsErrors = true; //WEB登录 loginCommand.SetHandler(BBDownLoginUtil.LoginWEB); //TV登录 loginTVCommand.SetHandler(BBDownLoginUtil.LoginTV); var parser = new CommandLineBuilder(rootCommand) .UseDefaults() .EnablePosixBundling(false) .UseExceptionHandler((ex, context) => { LogError(ex.Message); try { Console.CursorVisible = true; } catch { } Thread.Sleep(3000); Environment.Exit(1); }, 1) .Build(); var newArgsList = new List(); var commandLineResult = rootCommand.Parse(args); //显式抛出异常 if (commandLineResult.Errors.Any()) { Console.ForegroundColor = ConsoleColor.Red; Console.Error.WriteLine(commandLineResult.Errors.First().Message); Console.ResetColor(); Console.Error.WriteLine($"请使用 BBDown --help 查看帮助"); return 1; } if (commandLineResult.CommandResult.Command.Name.ToLower() != Path.GetFileNameWithoutExtension(Environment.ProcessPath)!.ToLower() && Path.GetFileNameWithoutExtension(Environment.ProcessPath)!.ToLower() != "dotnet") { // 服务器模式需要完整的arg列表 if (commandLineResult.CommandResult.Command.Name.ToLower() == "serve") { return await parser.InvokeAsync(args.ToArray()); } newArgsList.Add(commandLineResult.CommandResult.Command.Name); return await parser.InvokeAsync(newArgsList.ToArray()); } foreach (var item in commandLineResult.CommandResult.Children) { if (item is ArgumentResult a) { newArgsList.Add(a.Tokens[0].Value); } else if (item is OptionResult o) { newArgsList.Add("--" + o.Option.Name); newArgsList.AddRange(o.Tokens.Select(t => t.Value)); } } if (newArgsList.Contains("--debug")) { Config.DEBUG_LOG = true; } Console.BackgroundColor = ConsoleColor.DarkBlue; Console.ForegroundColor = ConsoleColor.White; var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!; Console.Write($"BBDown version {ver.Major}.{ver.Minor}.{ver.Build}, Bilibili Downloader.\r\n"); Console.ResetColor(); Console.Write("遇到问题请首先到以下地址查阅有无相关信息:\r\nhttps://github.com/nilaoda/BBDown/issues\r\n"); Console.WriteLine(); //处理配置文件 BBDownConfigParser.HandleConfig(newArgsList, rootCommand); return await parser.InvokeAsync(newArgsList.ToArray()); } private static Task RunApp(MyOption myOption) { //检测更新 _ = CheckUpdateAsync(); return DoWorkAsync(myOption); } private static void StartServer(string? listenUrl) { var defaultListenUrl = "http://0.0.0.0:23333"; //检测更新 _ = CheckUpdateAsync(); var server = new BBDownApiServer(); server.SetUpServer(); server.Run(string.IsNullOrEmpty(listenUrl) ? defaultListenUrl : listenUrl); } public static (Dictionary encodingPriority, Dictionary dfnPriority, string? firstEncoding, bool downloadDanmaku, BBDownDanmakuFormat[] downloadDanmakuFormats, string input, string savePathFormat, string lang, string aidOri, int delay) SetUpWork(MyOption myOption) { //处理废弃选项 HandleDeprecatedOptions(myOption); //处理冲突选项 HandleConflictingOptions(myOption); //寻找并设置所需的二进制文件路径 FindBinaries(myOption); //切换工作目录 ChangeWorkingDir(myOption); //解析优先级 var encodingPriority = ParseEncodingPriority(myOption, out var firstEncoding); var dfnPriority = ParseDfnPriority(myOption); //优先使用用户设置的UA HTTPUtil.UserAgent = string.IsNullOrEmpty(myOption.UserAgent) ? HTTPUtil.UserAgent : myOption.UserAgent; bool downloadDanmaku = myOption.DownloadDanmaku || myOption.DanmakuOnly; BBDownDanmakuFormat[] downloadDanmakuFormats = ParseDownloadDanmakuFormats(myOption); string input = myOption.Url; string savePathFormat = myOption.FilePattern; string lang = myOption.Language; string aidOri = ""; //原始aid int delay = Convert.ToInt32(myOption.DelayPerPage); Config.DEBUG_LOG = myOption.Debug; Config.HOST = myOption.Host; Config.EPHOST = myOption.EpHost; Config.TVHOST = myOption.TvHost; Config.AREA = myOption.Area; Config.COOKIE = myOption.Cookie; Config.TOKEN = myOption.AccessToken.Replace("access_token=", ""); LogDebug("AppDirectory: {0}", APP_DIR); LogDebug("运行参数:{0}", JsonSerializer.Serialize(myOption, MyOptionJsonContext.Default.MyOption)); return (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, aidOri, delay); } public static async Task<(string fetchedAid, VInfo vInfo, string apiType)> GetVideoInfoAsync(MyOption myOption, string aidOri, string input) { // 加载认证信息 LoadCredentials(myOption); // 检测是否登录了账号 if (myOption is { UseIntlApi: false, UseTvApi: false } && Config.AREA == "") { Log("检测账号登录..."); if (!await CheckLogin(Config.COOKIE)) { LogWarn("你尚未登录B站账号, 解析可能受到限制"); } } Log("获取aid..."); aidOri = await GetAvIdAsync(input); Log($"获取aid结束: {aidOri}"); if (string.IsNullOrEmpty(aidOri)) { throw new Exception("输入有误"); } Log("获取视频信息..."); IFetcher fetcher = FetcherFactory.CreateFetcher(aidOri, myOption.UseIntlApi); VInfo? vInfo = null; // 只输入 EP/SS 时优先按番剧查找,如果找不到则尝试按课程查找 try { vInfo = await fetcher.FetchAsync(aidOri); } catch (KeyNotFoundException e) { if (e.Message != "Arg_KeyNotFound") throw; // 错误消息不符合预期,抛出异常 if (aidOri.StartsWith("cheese:")) throw; // 已经按课程查找过,不再重复尝试 LogWarn("未找到此 EP/SS 对应番剧信息, 正在尝试按课程查找。"); aidOri = aidOri.Replace("ep", "cheese"); Log("新的 aid: " + aidOri); if (string.IsNullOrEmpty(aidOri)) { throw new Exception("输入有误"); } Log("获取视频信息..."); fetcher = FetcherFactory.CreateFetcher(aidOri, myOption.UseIntlApi); vInfo = await fetcher.FetchAsync(aidOri); } string title = vInfo.Title; long pubTime = vInfo.PubTime; LogColor("视频标题: " + title); if (pubTime != 0) { Log("发布时间: " + FormatTimeStamp(pubTime, "yyyy-MM-dd HH:mm:ss zzz")); } var bvid = vInfo.PagesInfo.FirstOrDefault()?.bvid; if (!string.IsNullOrEmpty(bvid) && !myOption.UseIntlApi) { Log($"视频URL: https://www.bilibili.com/video/{bvid}/"); } var mid = vInfo.PagesInfo.FirstOrDefault(p => !string.IsNullOrEmpty(p.ownerMid))?.ownerMid; if (!string.IsNullOrEmpty(mid)) { Log($"UP主页: https://space.bilibili.com/{mid}"); } if (vInfo.IsSteinGate && myOption.UseTvApi) { Log("视频为互动视频,暂时不支持tv下载,修改为默认下载"); myOption.UseTvApi = false; } string apiType = myOption.UseTvApi ? "TV" : (myOption.UseAppApi ? "APP" : (myOption.UseIntlApi ? "INTL" : "WEB")); //打印分P信息 List pagesInfo = vInfo.PagesInfo; bool more = false; foreach (Page p in pagesInfo) { if (!myOption.ShowAll) { if (more && p.index != pagesInfo.Count) continue; if (!more && p.index > 5) { Log("......"); more = true; continue; } } Log($"P{p.index}: [{p.cid}] [{p.title}] [{FormatTime(p.dur)}]"); } return (aidOri, vInfo, apiType); } public static async Task DownloadPagesAsync(MyOption myOption, VInfo vInfo, Dictionary encodingPriority, Dictionary dfnPriority, string? firstEncoding, bool downloadDanmaku, BBDownDanmakuFormat[] downloadDanmakuFormats, string input, string savePathFormat, string lang, string aidOri, int delay, string apiType, DownloadTask? relatedTask = null) { List pagesInfo = vInfo.PagesInfo; bool bangumi = vInfo.IsBangumi; bool cheese = vInfo.IsCheese; //获取已选择的分P列表 List? selectedPages = GetSelectedPages(myOption, vInfo, input); Log($"共计 {pagesInfo.Count} 个分P, 已选择:" + (selectedPages == null ? "ALL" : string.Join(",", selectedPages))); var pagesCount = pagesInfo.Count; //过滤不需要的分P if (selectedPages != null) { pagesInfo = pagesInfo.Where(p => selectedPages.Contains(p.index.ToString())).ToList(); } // 根据p数选择存储路径 savePathFormat = string.IsNullOrEmpty(myOption.FilePattern) ? SinglePageDefaultSavePath : myOption.FilePattern; // 1. 多P; 2. 只有1P, 但是是番剧, 尚未完结时 按照多P处理 if (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) { savePathFormat = string.IsNullOrEmpty(myOption.MultiFilePattern) ? MultiPageDefaultSavePath : myOption.MultiFilePattern; } foreach (Page p in pagesInfo) { if (pagesInfo.Count > 1 && delay > 0) { Log($"停顿{delay}秒..."); await Task.Delay(delay * 1000); } Log($"开始解析P{p.index}: {p.aid}... ({pagesInfo.IndexOf(p) + 1} of {pagesInfo.Count})"); if (myOption.SaveArchivesToFile) { if (CheckAidFromFile(p.aid)) { Log($"aid: {p.aid}已下载过, 跳过下载..."); continue; } } await DownloadPageAsync(p, myOption, vInfo, pagesInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, aidOri, apiType, relatedTask); if (myOption.SaveArchivesToFile) { SaveAidToFile(p.aid); } } Log("任务完成"); } private static async Task DownloadPageAsync(Page p, MyOption myOption, VInfo vInfo, List selectedPagesInfo, Dictionary encodingPriority, Dictionary dfnPriority, string? firstEncoding, bool downloadDanmaku, BBDownDanmakuFormat[] downloadDanmakuFormats, string input, string savePathFormat, string lang, string aidOri, string apiType, DownloadTask? relatedTask = null) { string desc = string.IsNullOrEmpty(p.desc) ? vInfo.Desc : p.desc; bool bangumi = vInfo.IsBangumi; var pagesCount = selectedPagesInfo.Count; List subtitleInfo = []; string title = vInfo.Title; string pic = vInfo.Pic; long pubTime = vInfo.PubTime; bool selected = false; //用户是否已经手动选择过了轨道 int retryCount = 0; downloadPage: try { LogDebug("尝试获取章节信息..."); p.points = await FetchPointsAsync(p.cid, p.aid); string videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4"; string audioPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.m4a"; var coverPath = $"{p.aid}/{p.aid}.jpg"; //处理文件夹以.结尾导致的异常情况 if (title.EndsWith('.')) title += "_fix"; //处理文件夹以.开头导致的异常情况 if (title.StartsWith('.')) title = "_" + title; //处理封面&&字幕 if (!myOption.OnlyShowInfo) { if (!Directory.Exists(p.aid)) { Directory.CreateDirectory(p.aid); } if (!myOption.SkipCover && !myOption.SubOnly && !File.Exists(coverPath) && !myOption.DanmakuOnly && !myOption.CoverOnly) { await DownloadFileAsync(pic == "" ? p.cover! : pic, coverPath, new DownloadConfig()); } if (!myOption.SkipSubtitle && !myOption.DanmakuOnly && !myOption.CoverOnly) { LogDebug("获取字幕..."); subtitleInfo = await SubUtil.GetSubtitlesAsync(p.aid, p.cid, p.epid, p.index, myOption.UseIntlApi); if (myOption.SkipAi && subtitleInfo.Any()) { Log($"跳过下载AI字幕"); subtitleInfo = subtitleInfo.Where(s => !s.lan.StartsWith("ai-")).ToList(); } foreach (Subtitle s in subtitleInfo) { Log($"下载字幕 {s.lan} => {SubUtil.GetSubtitleCode(s.lan).Item2}..."); LogDebug("下载:{0}", s.url); await SubUtil.SaveSubtitleAsync(s.url, s.path); if (myOption.SubOnly && File.Exists(s.path) && File.ReadAllText(s.path) != "") { var _outSubPath = FormatSavePath(savePathFormat, title, null, null, p, pagesCount, apiType, pubTime); if (_outSubPath.Contains('/')) { if (!Directory.Exists(_outSubPath.Split('/').First())) Directory.CreateDirectory(_outSubPath.Split('/').First()); } _outSubPath = Path.ChangeExtension(_outSubPath, $".{s.lan}.srt"); File.Move(s.path, _outSubPath, true); } } } if (myOption.SubOnly) { if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); return; } } //调用解析 ParsedResult parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding); List audioMaterial = []; if (!p.points.Any()) { p.points = parsedResult.ExtraPoints; } if (Config.DEBUG_LOG) { File.WriteAllText($"debug_{DateTime.Now:yyyyMMddHHmmssfff}.json", parsedResult.WebJsonString); } var savePath = ""; var downloadConfig = new DownloadConfig() { UseAria2c = myOption.UseAria2c, Aria2cArgs = myOption.Aria2cArgs, ForceHttp = myOption.ForceHttp, MultiThread = myOption.MultiThread, RelatedTask = relatedTask, }; //此处代码简直灾难, 后续优化吧 if ((parsedResult.VideoTracks.Any() || parsedResult.AudioTracks.Any()) && !parsedResult.Clips.Any()) //dash { if (parsedResult.VideoTracks.Count == 0) { LogWarn("没有找到符合要求的视频流"); if (myOption.VideoOnly) return; } if (parsedResult.AudioTracks.Count == 0) { LogWarn("没有找到符合要求的音频流"); if (myOption.AudioOnly) return; } if (myOption.AudioOnly) { parsedResult.VideoTracks.Clear(); } if (myOption.VideoOnly) { parsedResult.AudioTracks.Clear(); parsedResult.BackgroundAudioTracks.Clear(); parsedResult.RoleAudioList.Clear(); } //排序 parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending); parsedResult.AudioTracks = SortTracks(parsedResult.AudioTracks, encodingPriority, myOption.AudioAscending); parsedResult.BackgroundAudioTracks = SortTracks(parsedResult.BackgroundAudioTracks, encodingPriority, myOption.AudioAscending); foreach (var role in parsedResult.RoleAudioList) { role.audio = SortTracks(role.audio, encodingPriority, myOption.AudioAscending); } //打印轨道信息 if (!myOption.HideStreams) { PrintAllTracksInfo(parsedResult, p.dur, myOption.OnlyShowInfo); } //仅展示 跳过下载 if (myOption.OnlyShowInfo) { return; } int vIndex = 0; //用户手动选择的视频序号 int aIndex = 0; //用户手动选择的音频序号 //选择轨道 if (myOption.Interactive && !selected) { SelectTrackManually(parsedResult, ref vIndex, ref aIndex); selected = true; } Video? selectedVideo = parsedResult.VideoTracks.ElementAtOrDefault(vIndex); Audio? selectedAudio = parsedResult.AudioTracks.ElementAtOrDefault(aIndex); Audio? selectedBackgroundAudio = parsedResult.BackgroundAudioTracks.ElementAtOrDefault(aIndex); LogDebug("Format Before: " + savePathFormat); savePath = FormatSavePath(savePathFormat, title, selectedVideo, selectedAudio, p, pagesCount, apiType, pubTime); LogDebug("Format After: " + savePath); if (downloadDanmaku) { var danmakuXmlPath = Path.ChangeExtension(savePath, ".xml"); var danmakuAssPath = Path.ChangeExtension(savePath, ".ass"); Log("正在下载弹幕Xml文件"); var danmakuUrl = $"https://comment.bilibili.com/{p.cid}.xml"; await DownloadFileAsync(danmakuUrl, danmakuXmlPath, downloadConfig); var danmakus = DanmakuUtil.ParseXml(danmakuXmlPath); if (danmakus == null) { Log("弹幕Xml解析失败, 删除Xml..."); File.Delete(danmakuXmlPath); } else if (danmakus.Length == 0) { Log("当前视频没有弹幕, 删除Xml..."); File.Delete(danmakuXmlPath); } else if (downloadDanmakuFormats.Contains(BBDownDanmakuFormat.Ass)) { Log("正在保存弹幕Ass文件..."); await DanmakuUtil.SaveAsAssAsync(danmakus, danmakuAssPath); } // delete xml if possible if (!downloadDanmakuFormats.Contains(BBDownDanmakuFormat.Xml) && File.Exists(danmakuXmlPath)) { File.Delete(danmakuXmlPath); } if (myOption.DanmakuOnly) { if (Directory.Exists(p.aid)) { Directory.Delete(p.aid); } return; } } if (myOption.CoverOnly) { var coverUrl = pic == "" ? p.cover! : pic; var newCoverPath = Path.ChangeExtension(savePath, Path.GetExtension(coverUrl)); await DownloadFileAsync(coverUrl, newCoverPath, downloadConfig); if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); relatedTask?.SavePaths.Add(newCoverPath); } Log($"已选择的流:"); PrintSelectedTrackInfo(selectedVideo, selectedAudio, p.dur); //用户开启了强制替换 if (myOption.ForceReplaceHost && string.IsNullOrEmpty(myOption.UposHost)) { myOption.UposHost = BACKUP_HOST; } //处理PCDN HandlePcdn(myOption, selectedVideo, selectedAudio); if (!myOption.OnlyShowInfo && File.Exists(savePath) && new FileInfo(savePath).Length != 0) { Log($"{savePath}已存在, 跳过下载..."); relatedTask?.SavePaths.Add(savePath); File.Delete(coverPath); if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) { Directory.Delete(p.aid, true); } return; } if (selectedVideo != null) { //杜比视界, 若ffmpeg版本小于5.0, 使用mp4box封装 if (selectedVideo.dfn == Config.qualitys["126"] && !myOption.UseMP4box && !CheckFFmpegDOVI()) { LogWarn($"检测到杜比视界清晰度且您的ffmpeg版本小于5.0,将使用mp4box混流..."); myOption.UseMP4box = true; } Log($"开始下载P{p.index}视频..."); await DownloadTrackAsync(selectedVideo.baseUrl, videoPath, downloadConfig, video: true); } if (selectedAudio != null) { Log($"开始下载P{p.index}音频..."); await DownloadTrackAsync(selectedAudio.baseUrl, audioPath, downloadConfig, video: false); } if (selectedBackgroundAudio != null) { var backgroundPath = $"{p.aid}/{p.aid}.{p.cid}.P{p.index}.back_ground.m4a"; Log($"开始下载P{p.index}背景配音..."); await DownloadTrackAsync(selectedBackgroundAudio.baseUrl, backgroundPath, downloadConfig, video: false); audioMaterial.Add(new AudioMaterial("背景音频", "", backgroundPath)); } if (parsedResult.RoleAudioList.Any()) { foreach (var role in parsedResult.RoleAudioList) { Log($"开始下载P{p.index}配音[{role.title}]..."); await DownloadTrackAsync(role.audio[aIndex].baseUrl, role.path, downloadConfig, video: false); audioMaterial.Add(new AudioMaterial(role)); } } Log($"下载P{p.index}完毕"); if (!parsedResult.VideoTracks.Any()) videoPath = ""; if (!parsedResult.AudioTracks.Any()) audioPath = ""; if (myOption.SkipMux) return; Log($"开始合并音视频{(subtitleInfo.Any() ? "和字幕" : "")}..."); if (myOption.AudioOnly) savePath = savePath[..^4] + ".m4a"; var isHevc = selectedVideo?.codecs == "HEVC"; int code = BBDownMuxer.MuxAV(myOption.UseMP4box, p.bvid, videoPath, audioPath, audioMaterial, savePath, desc, title, p.ownerName ?? "", (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "", File.Exists(coverPath) ? coverPath : "", lang, subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux, isHevc); if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0) { LogError("合并失败"); return; } Log("清理临时文件..."); Thread.Sleep(200); if (parsedResult.VideoTracks.Any()) File.Delete(videoPath); if (parsedResult.AudioTracks.Any()) File.Delete(audioPath); if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters")); foreach (var s in subtitleInfo) File.Delete(s.path); foreach (var a in audioMaterial) File.Delete(a.path); if (selectedPagesInfo.Count == 1 || p.index == selectedPagesInfo.Last().index || p.aid != selectedPagesInfo.Last().aid) File.Delete(coverPath); if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); } else if (parsedResult.Clips.Any() && parsedResult.Dfns.Any()) //flv { bool flag = false; var clips = parsedResult.Clips; var dfns = parsedResult.Dfns; reParse: //排序 parsedResult.VideoTracks = SortTracks(parsedResult.VideoTracks, dfnPriority, encodingPriority, myOption.VideoAscending); int vIndex = 0; if (myOption.Interactive && !flag && !selected) { int i = 0; dfns.ForEach(key => LogColor($"{i++}.{Config.qualitys[key]}")); Log("请选择最想要的清晰度(输入序号): ", false); Console.ForegroundColor = ConsoleColor.Cyan; vIndex = Convert.ToInt32(Console.ReadLine()); if (vIndex > dfns.Count || vIndex < 0) vIndex = 0; Console.ResetColor(); //重新解析 parsedResult.VideoTracks.Clear(); parsedResult = await ExtractTracksAsync(aidOri, p.aid, p.cid, p.epid, myOption.UseTvApi, myOption.UseIntlApi, myOption.UseAppApi, firstEncoding, dfns[vIndex]); if (!p.points.Any()) p.points = parsedResult.ExtraPoints; flag = true; selected = true; goto reParse; } Log($"共计{parsedResult.VideoTracks.Count}条流(共有{clips.Count}个分段)."); int index = 0; foreach (var v in parsedResult.VideoTracks) { LogColor($"{index++}. [{v.dfn}] [{v.res}] [{v.codecs}] [{v.fps}] [~{v.size / 1024 / v.dur * 8:00} kbps] [{FormatFileSize(v.size)}]".Replace("[] ", ""), false); if (myOption.OnlyShowInfo) { clips.ForEach(Console.WriteLine); } } if (myOption.OnlyShowInfo) return; savePath = FormatSavePath(savePathFormat, title, parsedResult.VideoTracks.ElementAtOrDefault(vIndex), null, p, pagesCount, apiType, pubTime); if (File.Exists(savePath) && new FileInfo(savePath).Length != 0) { Log($"{savePath}已存在, 跳过下载..."); relatedTask?.SavePaths.Add(savePath); if (selectedPagesInfo.Count == 1 && Directory.Exists(p.aid)) { Directory.Delete(p.aid, true); } return; } var pad = string.Empty.PadRight(clips.Count.ToString().Length, '0'); for (int i = 0; i < clips.Count; i++) { var link = clips[i]; videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.{i.ToString(pad)}.mp4"; Log($"开始下载P{p.index}视频, 片段({(i + 1).ToString(pad)}/{clips.Count})..."); await DownloadTrackAsync(link, videoPath, downloadConfig, video: true); } Log($"下载P{p.index}完毕"); Log("开始合并分段..."); var files = GetFiles(Path.GetDirectoryName(videoPath)!, ".mp4"); videoPath = $"{p.aid}/{p.aid}.P{p.index}.{p.cid}.mp4"; BBDownMuxer.MergeFLV(files, videoPath); if (myOption.SkipMux) return; Log($"开始混流视频{(subtitleInfo.Any() ? "和字幕" : "")}..."); if (myOption.AudioOnly) savePath = savePath[..^4] + ".m4a"; int code = BBDownMuxer.MuxAV(false, p.bvid, videoPath, "", audioMaterial, savePath, desc, title, p.ownerName ?? "", (pagesCount > 1 || (bangumi && !vInfo.IsBangumiEnd)) ? p.title : "", File.Exists(coverPath) ? coverPath : "", lang, subtitleInfo, myOption.AudioOnly, myOption.VideoOnly, p.points, p.pubTime, myOption.SimplyMux); if (code != 0 || !File.Exists(savePath) || new FileInfo(savePath).Length == 0) { LogError("合并失败"); return; } Log("清理临时文件..."); Thread.Sleep(200); if (parsedResult.VideoTracks.Count != 0) File.Delete(videoPath); foreach (var s in subtitleInfo) File.Delete(s.path); foreach (var a in audioMaterial) File.Delete(a.path); if (p.points.Any()) File.Delete(Path.Combine(Path.GetDirectoryName(string.IsNullOrEmpty(videoPath) ? audioPath : videoPath)!, "chapters")); if (selectedPagesInfo.Count == 1 || p.index == selectedPagesInfo.Last().index || p.aid != selectedPagesInfo.Last().aid) File.Delete(coverPath); if (Directory.Exists(p.aid) && Directory.GetFiles(p.aid).Length == 0) Directory.Delete(p.aid, true); } else { LogError("解析此分P失败(建议--debug查看详细信息)"); if (parsedResult.WebJsonString.Length < 100) { LogError(parsedResult.WebJsonString); } LogDebug("{0}", parsedResult.WebJsonString); } if (!string.IsNullOrWhiteSpace(savePath)) { relatedTask?.SavePaths.Add(savePath); } } catch (Exception ex) { if (++retryCount > 2) throw; LogError(ex.Message); LogWarn("下载出现异常, 3秒后将进行自动重试..."); await Task.Delay(3000); goto downloadPage; } } private static async Task DoWorkAsync(MyOption myOption) { try { var (encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, aidOri, delay) = SetUpWork(myOption); var (fetchedAid, vInfo, apiType) = await GetVideoInfoAsync(myOption, aidOri, input); await DownloadPagesAsync(myOption, vInfo, encodingPriority, dfnPriority, firstEncoding, downloadDanmaku, downloadDanmakuFormats, input, savePathFormat, lang, fetchedAid, delay, apiType); } catch (Exception e) { Console.BackgroundColor = ConsoleColor.Red; Console.ForegroundColor = ConsoleColor.White; var msg = Config.DEBUG_LOG ? e.ToString() : e.Message; Console.Write($"{msg}{Environment.NewLine}请尝试升级到最新版本后重试!"); Console.ResetColor(); Console.WriteLine(); Thread.Sleep(1); Environment.Exit(1); } } private static List