Full Code of nilaoda/BBDown for AI

master 259a5558cee0 cached
60 files
308.8 KB
79.7k tokens
281 symbols
1 requests
Download .txt
Showing preview only (326K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <Version>1.6.3</Version>
    <Description>BBDown是一个免费且便捷高效的哔哩哔哩下载/解析软件.</Description>
    <PackageProjectUrl>https://github.com/nilaoda/BBDown</PackageProjectUrl>
    <StartupObject></StartupObject>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>BBDown</ToolCommandName>
    <PackageOutputPath>./nupkg</PackageOutputPath>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="QRCoder" Version="1.6.0" />
    <PackageReference Include="SharpZipLib" Version="1.4.2" />
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\BBDown.Core\BBDown.Core.csproj" />
  </ItemGroup>

</Project>


================================================
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<DownloadTask> runningTasks = [];
    private readonly List<DownloadTask> 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<ServeRequestOptions> 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<DownloadTask> 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<string> SavePaths = new();
};
public record DownloadTaskCollection(List<DownloadTask> Running, List<DownloadTask> Finished);

record struct MyOptionBindingResult<T>(T? Result, Exception? Exception)
{
    public bool IsValid => Exception is null;

    public static async ValueTask<MyOptionBindingResult<T>> 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<DownloadTask>))]
[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<int> 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<string> 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<int, long, long> 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<Clip> allClips = GetAllClips(url, fileSize);
        int total = allClips.Count;
        LogDebug("分段数量:{0}", total);
        ConcurrentDictionary<int, long> 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<Clip> GetAllClips(string url, long fileSize)
    {
        List<Clip> 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<long> 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;
    }

    /// <summary>
    /// 将下载地址强制转换为HTTP
    /// </summary>
    /// <param name="url"></param>
    /// <returns></returns>
    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<string> 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<Subtitle>? subs, bool audioOnly, bool videoOnly, List<ViewPoint>? 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> audioMaterial, string outPath, string desc = "", string title = "", string author = "", string episodeId = "", string pic = "", string lang = "", List<Subtitle>? subs = null, bool audioOnly = false, bool videoOnly = false, List<ViewPoint>? 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<string> 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";
    }

    /// <summary>
    /// 通过avid检测是否为版权内容, 如果是的话返回ep:xx格式
    /// </summary>
    /// <param name="avid"></param>
    /// <returns></returns>
    private static async Task<string> 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<string> 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<string> 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<string> 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;
    }

    /// <summary>
    /// 输入一堆已存在的文件, 合并到新文件
    /// </summary>
    /// <param name="files"></param>
    /// <param name="outputFilePath"></param>
    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);
    }

    /// <summary>
    /// 寻找指定目录下指定后缀的文件的详细路径 如".txt"
    /// </summary>
    /// <param name="dir"></param>
    /// <param name="ext"></param>
    /// <returns></returns>
    public static string[] GetFiles(string dir, string ext)
    {
        List<string> 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;
    }


    /// <summary>
    /// 获取url字符串参数, 返回参数值字符串
    /// </summary>
    /// <param name="name">参数名称</param>
    /// <param name="url">url字符串</param>
    /// <returns></returns>
    public static string GetQueryString(string name, string url)
    {
        Regex re = QueryRegex();
        MatchCollection mc = re.Matches(url);
        foreach (Match m in mc.Cast<Match>())
        {
            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<string, string> ToDictionary(this NameValueCollection nameValueCollection)
    {
        var dict = new Dictionary<string, string>();
        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;
    }

    /// <summary>
    /// 检测ffmpeg是否识别杜比视界
    /// </summary>
    /// <returns></returns>
    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;
    }

    /// <summary>
    /// 获取章节信息
    /// </summary>
    /// <param name="cid"></param>
    /// <param name="aid"></param>
    /// <returns></returns>
    public static async Task<List<ViewPoint>> FetchPointsAsync(string cid, string aid)
    {
        var ponints = new List<ViewPoint>();
        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;
    }

    /// <summary>
    /// 生成metadata文件, 用于ffmpeg混流章节信息
    /// </summary>
    /// <param name="points"></param>
    /// <returns></returns>
    public static string GetFFmpegMetaString(List<ViewPoint> 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();
    }

    /// <summary>
    /// 生成metadata文件, 用于mp4box混流章节信息
    /// </summary>
    /// <param name="points"></param>
    /// <returns></returns>
    public static string GetMp4boxMetaString(List<ViewPoint> 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<bool> 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<string> Url = new("url", description: "视频地址 或 av|bv|BV|ep|ss");
    private static readonly Option<bool> UseTvApi = new(["--use-tv-api", "-tv"], "使用TV端解析模式");
    private static readonly Option<bool> UseAppApi = new(["--use-app-api", "-app"], "使用APP端解析模式");
    private static readonly Option<bool> UseIntlApi = new(["--use-intl-api", "-intl"], "使用国际版(东南亚视频)解析模式");
    private static readonly Option<bool> UseMP4box = new(["--use-mp4box"], "使用MP4Box来混流");
    private static readonly Option<string> EncodingPriority = new(["--encoding-priority", "-e"], "视频及音频编码的选择优先级, 用逗号分割 例: \"hevc,av1,avc,flac,eac3,m4a\"");
    private static readonly Option<string> DfnPriority = new(["--dfn-priority", "-q"], "画质优先级,用逗号分隔 例: \"8K 超高清, 1080P 高码率, HDR 真彩, 杜比视界\"");
    private static readonly Option<bool> OnlyShowInfo = new(["--only-show-info", "-info"], "仅解析而不进行下载");
    private static readonly Option<bool> HideStreams = new(["--hide-streams", "-hs"], "不要显示所有可用音视频流");
    private static readonly Option<bool> Interactive = new(["--interactive", "-ia"], "交互式选择清晰度");
    private static readonly Option<bool> ShowAll = new(["--show-all"], "展示所有分P标题");
    private static readonly Option<bool> UseAria2c = new(["--use-aria2c", "-aria2"], "调用aria2c进行下载(你需要自行准备好二进制可执行文件)");
    private static readonly Option<string> Aria2cArgs = new(["--aria2c-args"], "调用aria2c的附加参数(默认参数包含\"-x16 -s16 -j16 -k 5M\", 使用时注意字符串转义)");
    private static readonly Option<bool> MultiThread = new(["--multi-thread", "-mt"], "使用多线程下载(默认开启)");
    private static readonly Option<string> 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<bool> SimplyMux = new(["--simply-mux"], "精简混流,不增加描述、作者等信息");
    private static readonly Option<bool> AudioOnly = new(["--audio-only"], "仅下载音频");
    private static readonly Option<bool> VideoOnly = new(["--video-only"], "仅下载视频");
    private static readonly Option<bool> DanmakuOnly = new(["--danmaku-only"], "仅下载弹幕");
    private static readonly Option<bool> CoverOnly = new(["--cover-only"], "仅下载封面");
    private static readonly Option<bool> SubOnly = new(["--sub-only"], "仅下载字幕");
    private static readonly Option<bool> Debug = new(["--debug"], "输出调试日志");
    private static readonly Option<bool> SkipMux = new(["--skip-mux"], "跳过混流步骤");
    private static readonly Option<bool> SkipSubtitle = new(["--skip-subtitle"], "跳过字幕下载");
    private static readonly Option<bool> SkipCover = new(["--skip-cover"], "跳过封面下载");
    private static readonly Option<bool> ForceHttp = new(["--force-http"], "下载音视频时强制使用HTTP协议替换HTTPS(默认开启)");
    private static readonly Option<bool> DownloadDanmaku = new(["--download-danmaku", "-dd"], "下载弹幕");
    private static readonly Option<string> DownloadDanmakuFormats = new(["--download-danmaku-formats", "-ddf"], $"指定需下载的弹幕格式, 用逗号分隔, 可选 {string.Join('/', BBDownDanmakuFormatInfo.AllFormatNames)}, 默认: \"{string.Join(',', BBDownDanmakuFormatInfo.AllFormatNames)}\"");
    private static readonly Option<bool> SkipAi = new(["--skip-ai"], description: "跳过AI字幕下载(默认开启)");
    private static readonly Option<bool> VideoAscending = new(["--video-ascending"], "视频升序(最小体积优先)");
    private static readonly Option<bool> AudioAscending = new(["--audio-ascending"], "音频升序(最小体积优先)");
    private static readonly Option<bool> AllowPcdn = new(["--allow-pcdn"], "不替换PCDN域名, 仅在正常情况与--upos-host均无法下载时使用");
    private static readonly Option<string> Language = new(["--language"], "设置混流的音频语言(代码), 如chi, jpn等");
    private static readonly Option<string> UserAgent = new(["--user-agent", "-ua"], "指定user-agent, 否则使用随机user-agent");
    private static readonly Option<string> Cookie = new(["--cookie", "-c"], "设置字符串cookie用以下载网页接口的会员内容");
    private static readonly Option<string> AccessToken = new(["--access-token", "-token"], "设置access_token用以下载TV/APP接口的会员内容");
    private static readonly Option<string> WorkDir = new(["--work-dir"], "设置程序的工作目录");
    private static readonly Option<string> FFmpegPath = new(["--ffmpeg-path"], "设置ffmpeg的路径");
    private static readonly Option<string> Mp4boxPath = new(["--mp4box-path"], "设置mp4box的路径");
    private static readonly Option<string> Aria2cPath = new(["--aria2c-path"], "设置aria2c的路径");
    private static readonly Option<string> UposHost = new(["--upos-host"], "自定义upos服务器");
    private static readonly Option<bool> ForceReplaceHost = new(["--force-replace-host"], "强制替换下载服务器host(默认开启)");
    private static readonly Option<bool> SaveArchivesToFile = new(["--save-archives-to-file"], "将下载过的视频记录到本地文件中, 用于后续跳过下载同个视频");
    private static readonly Option<string> DelayPerPage = new(["--delay-per-page"], "设置下载合集分P之间的下载间隔时间(单位: 秒, 默认无间隔)");
    private static readonly Option<string> FilePattern = new(["--file-pattern", "-F"], 
        $"使用内置变量自定义单P存储文件名:\r\n\r\n" + 
        $"<videoTitle>: 视频主标题\r\n" + 
        $"<pageNumber>: 视频分P序号\r\n" + 
        $"<pageNumberWithZero>: 视频分P序号(前缀补零)\r\n" + 
        $"<pageTitle>: 视频分P标题\r\n" + 
        $"<bvid>: 视频BV号\r\n" + 
        $"<aid>: 视频aid\r\n" + 
        $"<cid>: 视频cid\r\n" + 
        $"<dfn>: 视频清晰度\r\n" + 
        $"<res>: 视频分辨率\r\n" + 
        $"<fps>: 视频帧率\r\n" + 
        $"<videoCodecs>: 视频编码\r\n" + 
        $"<videoBandwidth>: 视频码率\r\n" + 
        $"<audioCodecs>: 音频编码\r\n" + 
        $"<audioBandwidth>: 音频码率\r\n" + 
        $"<ownerName>: 上传者名称\r\n" + 
        $"<ownerMid>: 上传者mid\r\n" + 
        $"<publishDate>: 收藏夹/番剧/合集发布时间\r\n" + 
        $"<videoDate>: 视频发布时间(分p视频发布时间与<publishDate>相同)\r\n" + 
        $"<apiType>: API类型(TV/APP/INTL/WEB)\r\n\r\n" + 
        $"默认为: {Program.SinglePageDefaultSavePath}\r\n");
    private static readonly Option<string> MultiFilePattern = new(["--multi-file-pattern", "-M"], $"使用内置变量自定义多P存储文件名:\r\n\r\n默认为: {Program.MultiPageDefaultSavePath}\r\n");
    private static readonly Option<string> Host = new(["--host"], "指定BiliPlus host(使用BiliPlus需要access_token, 不需要cookie, 解析服务器能够获取你账号的大部分权限!)");
    private static readonly Option<string> EpHost = new(["--ep-host"], "指定BiliPlus EP host(用于代理api.bilibili.com/pgc/view/web/season, 大部分解析服务器不支持代理该接口)");
    private static readonly Option<string> TvHost = new(["--tv-host"], "自定义tv端接口请求Host(用于代理api.snm0516.aisee.tv)");
    private static readonly Option<string> Area = new(["--area"], "(hk|tw|th) 使用BiliPlus时必选, 指定BiliPlus area");
    private static readonly Option<string> ConfigFile = new(["--config-file"], "读取指定的BBDown本地配置文件(默认为: BBDown.config)");//以下仅为兼容旧版本命令行, 不建议使用
    private static readonly Option<string> Aria2cProxy = new(["--aria2c-proxy"], "调用aria2c进行下载时的代理地址配置") { IsHidden = true };
    private static readonly Option<bool> OnlyHevc = new(["--only-hevc", "-hevc"], "只下载hevc编码") { IsHidden = true };
    private static readonly Option<bool> OnlyAvc = new(["--only-avc", "-avc"], "只下载avc编码") { IsHidden = true };
    private static readonly Option<bool> OnlyAv1 = new(["--only-av1", "-av1"], "只下载av1编码") { IsHidden = true };
    private static readonly Option<bool> AddDfnSubfix = new(["--add-dfn-subfix"], "为文件加入清晰度后缀, 如XXX[1080P 高码率]") { IsHidden = true };
    private static readonly Option<bool> NoPaddingPageNum = new(["--no-padding-page-num"], "不给分P序号补零") { IsHidden = true };
    private static readonly Option<bool> BandwithAscending = new(["--bandwith-ascending"], "比特率升序(最小体积优先)") { IsHidden = true };


    class MyOptionBinder : BinderBase<MyOption>
    {
        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<MyOption, Task> 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
================================================
<Project>

  <PropertyGroup>
    <IlcOptimizationPreference>Speed</IlcOptimizationPreference>
    <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
    <PublishAot>true</PublishAot>
    <EventSourceSupport>false</EventSourceSupport>
    <UseSystemResourceKeys>true</UseSystemResourceKeys>
    <InvariantGlobalization>true</InvariantGlobalization>
    <StripSymbols>true</StripSymbols>
    <ObjCopyName Condition="'$(RuntimeIdentifier)' == 'linux-arm64'">aarch64-linux-gnu-objcopy</ObjCopyName>
  </PropertyGroup>
  
</Project>


================================================
FILE: BBDown/Model/ServeRequestOptions.cs
================================================
using BBDown;

internal class ServeRequestOptions : MyOption
{

    /// <summary>
    /// 任务完成回调Http请求地址
    /// </summary>
    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
{

    /// <summary>
    /// 兼容旧版本命令行参数并给出警告
    /// </summary>
    /// <param name="myOption"></param>
    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 += "[<dfn>]";
                MultiPageDefaultSavePath += "[<dfn>]";
                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("<pageNumberWithZero>", "<pageNumber>");
                LogWarn($"已切换至 -M \"{MultiPageDefaultSavePath}\"");
            }
        }
        if (myOption.BandwithAscending)
        {
            LogWarn("--bandwith-ascending 已被弃用, 建议使用 --video-ascending 与 --audio-ascending 来指定视频或音频是否升序, 本次执行已将视频与音频均设为升序");
            myOption.VideoAscending = true;
            myOption.AudioAscending = true;
        }
    }

    /// <summary>
    /// 解析用户指定的编码优先级
    /// </summary>
    /// <param name="myOption"></param>
    /// <returns></returns>
    private static Dictionary<string, byte> ParseEncodingPriority(MyOption myOption, out string firstEncoding)
    {
        var encodingPriority = new Dictionary<string, byte>();
        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();
    }

    /// <summary>
    /// 解析用户输入的清晰度规格优先级
    /// </summary>
    /// <param name="myOption"></param>
    /// <returns></returns>
    private static Dictionary<string, int> ParseDfnPriority(MyOption myOption)
    {
        var dfnPriority = new Dictionary<string, int>();
        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;
    }

    /// <summary>
    /// 寻找并设置所需的二进制文件
    /// </summary>
    /// <param name="myOption"></param>
    /// <exception cref="Exception"></exception>
    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;
            }

        }
    }

    /// <summary>
    /// 处理有冲突的选项
    /// </summary>
    /// <param name="myOption"></param>
    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;
        }
    }

    /// <summary>
    /// 设置用户输入的自定义工作目录
    /// </summary>
    /// <param name="myOption"></param>
    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);
        }
    }

    /// <summary>
    /// 加载用户的认证信息(cookie或token)
    /// </summary>
    /// <param name="myOption"></param>
    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);
        }
    }

    /// <summary>
    /// 获取选中的分P列表
    /// </summary>
    /// <param name="myOption"></param>
    /// <param name="vInfo"></param>
    /// <param name="input"></param>
    /// <returns></returns>
    private static List<string>? GetSelectedPages(MyOption myOption, VInfo vInfo, string input)
    {
        List<string>? selectedPages = null;
        List<Page> 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<string>();

            //选择最新分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;
    }

    /// <summary>
    /// 处理CDN域名
    /// </summary>
    /// <param name="myOption"></param>
    /// <param name="video"></param>
    /// <param name="audio"></param>
    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}/");
            }
        }
    }

    /// <summary>
    /// 打印解析到的各个轨道信息
    /// </summary>
    /// <param name="parsedResult"></param>
    /// <param name="pageDur"></param>
    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);
        }
    }

    /// <summary>
    /// 引导用户进行手动选择轨道
    /// </summary>
    /// <param name="parsedResult"></param>
    /// <param name="vIndex"></param>
    /// <param name="aIndex"></param>
    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();
        }
    }

    /// <summary>
    /// 下载轨道
    /// </summary>
    /// <returns></returns>
    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; } = "<videoTitle>";
    public static string MultiPageDefaultSavePath { get; set; } = "<videoTitle>/[P<pageNumberWithZero>]<pageTitle>";

    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<int> 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<string>(
            ["--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<string>();
        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<string, byte> encodingPriority, Dictionary<string, int> 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<Page> 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<string, byte> encodingPriority, Dictionary<string, int> dfnPriority,
        string? firstEncoding, bool downloadDanmaku, BBDownDanmakuFormat[] downloadDanmakuFormats, string input, string savePathFormat, string lang, string aidOri, int delay, string apiType, DownloadTask? relatedTask = null)
    {
        List<Page> pagesInfo = vInfo.PagesInfo;
        bool bangumi = vInfo.IsBangumi;
        bool cheese = vInfo.IsCheese;
        //获取已选择的分P列表
        List<string>? 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<Page> selectedPagesInfo, Dictionary<string, byte> encodingPriority, Dictionary<string, int> 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<Subtitle> 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> 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<Video> SortTracks(List<Video> videoTracks, Dictionary<string, int> dfnPriority, Dictionary<string, byte> encodingPriority, bool videoAscending)
    {
        //用户同时输入了自定义分辨率优先级和自定义编码优先级, 则根据输入顺序依次进行排序
        return dfnPriority.Any() && encodingPriority.Any() && Environment.CommandLine.IndexOf("--encoding-priority", StringComparison.Ordinal) < Environment.CommandLine.IndexOf("--dfn-priority")
            ? videoTracks
                .OrderBy(v => encodingPriority.GetValueOrDefault(v.codecs, (byte)100))
                .ThenBy(v => dfnPriority.GetValueOrDefault(v.dfn, 100))
                .ThenByDescending(v => Convert.ToInt32(v.id))
                .ThenBy(v => videoAscending ? v.bandwith : -v.bandwith)
                .ToList()
            : videoTracks
                .OrderBy(v => dfnPriority.GetValueOrDefault(v.dfn, 100))
                .ThenBy(v => encodingPriority.GetValueOrDefault(v.codecs, (byte)100))
                .ThenByDescending(v => Convert.ToInt32(v.id))
                .ThenBy(v => videoAscending ? v.bandwith : -v.bandwith)
                .ToList();
    }
    
    private static List<Audio> SortTracks(List<Audio> audioTracks, Dictionary<string, byte> encodingPriority, bool audioAscending)
    {
        return audioTracks
            .OrderBy(a => encodingPriority.GetValueOrDefault(a.shortCodecs, (byte)100))
            .ThenBy(a => audioAscending ? a.bandwith : -a.bandwith)
            .ToList();
    }

    private static string FormatSavePath(string savePathFormat, string title, Video? videoTrack, Audio? audioTrack, Page p, int pagesCount, string apiType, long pubTime)
    {
        var result = savePathFormat.Replace('\\', '/');
        var regex = InfoRegex();
        foreach (Match m in regex.Matches(result).Cast<Match>())
        {
            var key = m.Groups[1].Value;

            //解析自定义日期格式
            var defaultDateFormat = "yyyy-MM-dd_HH-mm-ss";
            string[] prefixes = ["publishDate:", "videoDate:"];
            foreach (var prefix in prefixes)
            {
                if (key.StartsWith(prefix))
                {
                    defaultDateFormat = key[(key.IndexOf(':') + 1)..];
                    key = prefix.Replace(":", "");
                    break;
                }
            }

            var v = key switch
            {
                "videoTitle" => GetValidFileName(title, filterSlash: true).Trim().TrimEnd('.').Trim(),
                "pageNumber" => p.index.ToString(),
                "pageNumberWithZero" => p.index.ToString().PadLeft(pagesCount.ToString().Length, '0'),
                "pageTitle" => GetValidFileName(p.title, filterSlash: true).Trim().TrimEnd('.').Trim(),
                "bvid" => p.bvid,
                "aid" => p.aid,
                "cid" => p.cid,
                "ownerName" => p.ownerName == null ? "" : GetValidFileName(p.ownerName, filterSlash: true).Trim().TrimEnd('.').Trim(),
                "ownerMid" => p.ownerMid ?? "",
                "dfn" => videoTrack == null ? "" : videoTrack.dfn,
                "res" => videoTrack == null ? "" : videoTrack.res,
                "fps" => videoTrack == null ? "" : videoTrack.fps,
                "videoCodecs" => videoTrack == null ? "" : videoTrack.codecs,
                "videoBandwidth" => videoTrack == null ? "" : videoTrack.bandwith.ToString(),
                "audioCodecs" => audioTrack == null ? "" : audioTrack.codecs,
                "audioBandwidth" => audioTrack == null ? "" : audioTrack.bandwith.ToString(),
                "publishDate" => FormatTimeStamp(pubTime, defaultDateFormat),
                "videoDate" => FormatTimeStamp(p.pubTime, defaultDateFormat),
                "apiType" => apiType,
                _ => $"<{key}>"
            };
            result = result.Replace(m.Value, v);
        }
        if (!result.EndsWith(".mp4")) { result += ".mp4"; }
        return result;
    }

    [GeneratedRegex("<([\\w:\\-.]+?)>")]
    private static partial Regex InfoRegex();
}

================================================
FILE: BBDown/ProgressBar.cs
================================================
using System;
using System.Text;
using System.Threading;

/**
 * From https://gist.github.com/DanielSWolf/0ab6a96899cc5377bf54
 */
namespace BBDown;

class ProgressBar : IDisposable, IProgress<double>
{
	private const int blockCount = 40;
	private readonly TimeSpan animationInterval = TimeSpan.FromSeconds(1.0 / 8);
	private const string animation = @"|/-\";

	private readonly Timer timer;

	private double currentProgress = 0;
	private string currentText = string.Empty;
	private bool disposed = false;
	private int animationIndex = 0;

	//速度计算
	private readonly TimeSpan speedCalcInterval = TimeSpan.FromSeconds(1);
	private long lastDownloadedBytes = 0;
	private long downloadedBytes = 0;
	private string speedString = "";
	private readonly Timer speedTimer;

	//服务器模式使用,更新下载任务的进度
	private DownloadTask? RelatedTask = null;

	public ProgressBar(DownloadTask? task = null)
	{
		timer = new Timer(TimerHandler);
		speedTimer = new Timer(SpeedTimerHandler);
		if (task is not null) RelatedTask = task;
		// A progress bar is only for temporary display in a console window.
		// If the console output is redirected to a file, draw nothing.
		// Otherwise, we'll end up with a lot of garbage in the target file.
		// However, if this progressbar is for a server download task,
		// we still need it to report progress no matter where stdout is redirected.
		// The prevention of writing garbage should be controlled on the methods do the actual writing.
		if (!Console.IsOutputRedirected || RelatedTask is not null)
		{
			ResetTimer();
			ResetSpeedTimer();

		}
	}

	public void Report(double value)
	{
		// Make sure value is in [0..1] range
		value = Math.Max(0, Math.Min(1, value));
		Interlocked.Exchange(ref currentProgress, value);
	}

	public void Report(double value, long bytesCount)
	{
		// Make sure value is in [0..1] range
		value = Math.Max(0, Math.Min(1, value));
		Interlocked.Exchange(ref currentProgress, value);
		Interlocked.Exchange(ref downloadedBytes, bytesCount);
	}

	private void SpeedTimerHandler(object? state)
	{
		lock (speedTimer)
		{
			if (disposed) return;

			if (downloadedBytes > 0 && downloadedBytes - lastDownloadedBytes > 0)
			{
				var delta = downloadedBytes - lastDownloadedBytes;
				speedString = " - " + BBDownUtil.FormatFileSize(delta) + "/s";
				lastDownloadedBytes = downloadedBytes;
				if (RelatedTask is not null) 
				{
					RelatedTask.DownloadSpeed = delta;
					RelatedTask.TotalDownloadedBytes += delta;
				}
			}

			ResetSpeedTimer();
		}
	}

	private void TimerHandler(object? state)
	{
		lock (timer)
		{
			if (disposed) return;

			int progressBlockCount = (int)(currentProgress * blockCount);
			double percent = currentProgress * 100;
			string text = string.Format("                            [{0}{1}] {2,3:0.00}% {3}{4}",
				new string('#', progressBlockCount), new string('-', blockCount - progressBlockCount), percent,
				animation[animationIndex++ % animation.Length],
				speedString);
			UpdateText(text);
			if (RelatedTask is not null) 
			{
				RelatedTask.Progress = currentProgress;
			}

			ResetTimer();
		}
	}

	private void UpdateText(string text)
	{
		// Write nothing when output is redirected
		if (Console.IsOutputRedirected) return;
		// Get length of common portion
		int commonPrefixLength = 0;
		int commonLength = Math.Min(currentText.Length, text.Length);
		while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength])
		{
			commonPrefixLength++;
		}

		// Backtrack to the first differing character
		StringBuilder outputBuilder = new();
		outputBuilder.Append('\b', currentText.Length - commonPrefixLength);

		// Output new suffix
		outputBuilder.Append(text[commonPrefixLength..]);

		// If the new text is shorter than the old one: delete overlapping characters
		int overlapCount = currentText.Length - text.Length;
		if (overlapCount > 0)
		{
			outputBuilder.Append(' ', overlapCount);
			outputBuilder.Append('\b', overlapCount);
		}

		Console.Write(outputBuilder);
		currentText = text;
	}

	private void ResetTimer()
	{
		timer.Change(animationInterval, TimeSpan.FromMilliseconds(-1));
	}

	private void ResetSpeedTimer()
	{
		speedTimer.Change(speedCalcInterval, TimeSpan.FromMilliseconds(-1));
	}

	public void Dispose()
	{
		lock (timer)
		{
			disposed = true;
			UpdateText(string.Empty);
		}
	}
}

================================================
FILE: BBDown/Properties/launchSettings.json
================================================
{
  "profiles": {
    "BBDown": {
      "commandName": "Project",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:58682"
    },
    "BBDown.Server": {
      "commandName": "Project",
      "commandLineArgs": "serve",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:58682"
    }
  }
}

================================================
FILE: BBDown.Core/APP/Header/device.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message Device {
    optional int32 appId = 1;
    optional int32 build = 2;
    optional string buvid = 3;
    optional string mobiApp = 4;
    optional string platform = 5;
    optional string device = 6;
    optional string channel = 7;
    optional string brand = 8;
    optional string model = 9;
    optional string osver = 10;
    optional string fpLocal  = 11;
    optional string fpRemote  = 12;
    optional string versionName  = 13;
    optional string fp = 14;
}

================================================
FILE: BBDown.Core/APP/Header/fawkesreq.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message FawkesReq {
    optional string appkey = 1;
    optional string env = 2;
    optional string sessionId = 3;
}

================================================
FILE: BBDown.Core/APP/Header/locale.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message Locale {
    message LocaleIds {
        optional string language = 1;
        optional string script = 2;
        optional string region = 3;
    }
    optional LocaleIds cLocale = 1;
    optional LocaleIds sLocale = 2;
}

================================================
FILE: BBDown.Core/APP/Header/metadata.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message Metadata {
    optional string accessKey = 1;
    optional string mobiApp = 2;
    optional string device = 3;
    optional int32 build = 4;
    optional string channel = 5;
    optional string buvid = 6;
    optional string platform = 7;
}

================================================
FILE: BBDown.Core/APP/Header/network.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message Network {
    enum TYPE {
        NT_UNKNOWN = 0;
        WIFI = 1;
        CELLULAR = 2;
        OFFLINE = 3;
        OTHERNET = 4;
        ETHERNET = 5;
    }
    optional TYPE type = 1;
    enum TF {
        TF_UNKNOWN = 0;
        U_CARD = 1;
        U_PKG = 2;
        C_CARD = 3;
        C_PKG = 4;
        T_CARD = 5;
        T_PKG = 6;
    }
    optional TF tf = 2;
    optional string oid = 3;
}

================================================
FILE: BBDown.Core/APP/Header/restriction.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message Restriction {
    optional bool teenagersMode = 1;
    optional bool lessonsMode = 2;
    enum Mode {
        NORMAL = 0;
        TEENAGERS = 1;
        LESSONS = 2;
    }
    optional Mode mode = 3;
    optional bool review = 4;
}

================================================
FILE: BBDown.Core/APP/Payload/dmviewreq.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message DmViewReq {
    optional int64 pid = 1;
    optional int64 oid = 2;
    optional int32 type = 3;
    optional string spmid = 4;
    optional int32 isHardBoot = 5;
}

================================================
FILE: BBDown.Core/APP/Payload/playviewreq.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message PlayViewReq {
    optional int64 epId = 1;
    optional int64 cid = 2;
    optional int64 qn = 3;
    optional int32 fnver = 4;
    optional int32 fnval = 5;
    optional uint32 download = 6;
    optional int32 forceHost = 7;
    optional bool fourk = 8;
    optional string spmid = 9;
    optional string fromSpmid = 10;
    optional int32 teenagersMode = 11;
    enum CodeType {
        NOCODE = 0;
        CODE264 = 1;
        CODE265 = 2;
        CODEAV1 = 3;
    }
    optional CodeType preferCodecType = 12;
    optional bool isPreview = 13;
    optional int64 roomId = 14;
}

================================================
FILE: BBDown.Core/APP/Response/dmviewreply.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message DmViewReply {
    optional bool closed = 1;
    optional VideoMask mask = 2;
    optional VideoSubtitle subtitle = 3;
    repeated string specialDms = 4;
    optional DanmakuFlagConfig aiFlag = 5;
    optional DanmuPlayerViewConfig playerConfig = 6;
    optional int32 sendBoxStyle = 7;
    optional bool allow = 8;
    optional string checkBoxShowMsg = 9;
    optional bool checkBox = 10;
    optional string textPlaceholder = 11;
}

message VideoMask {
    optional int64 cid = 1;
    optional int32 plat = 2;
    optional int32 fps = 3;
    optional int64 time = 4;
    optional string maskUrl = 5;
}

message VideoSubtitle {
    optional string lan = 1;
    optional string lanDoc = 2;
    repeated SubtitleItem subtitles = 3;

}

message DanmakuFlagConfig {
    optional int32 recFlag = 1;
    optional string recText = 2;
    optional int32 recSwitch = 3;
}

message DanmuPlayerViewConfig {
    optional DanmuDefaultPlayerConfig danmukuDefaultPlayerConfig = 1;
    optional DanmuPlayerConfig danmukuPlayerConfig = 2;
    optional DanmuPlayerDynamicConfig danmukuPlayerDynamicConfig = 3;
}

message SubtitleItem {
    optional UserInfo author = 6;
    optional string idStr = 1;
    optional int64 id = 2;
    optional string lanDoc = 4;
    optional string lan = 3;
    optional string subtitleUrl = 5;
}

message UserInfo {
    optional string face = 4;
    optional int64 mid = 1;
    optional string name = 2;
    optional int32 rank = 6;
    optional string sex = 3;
    optional string sign = 5;
}

message DanmuDefaultPlayerConfig {
    optional bool playerDanmakuUseDefaultConfig = 1;
    optional bool playerDanmakuAiRecommendedSwitch = 4;
    optional int32 playerDanmakuAiRecommendedLevel = 5;
    optional bool playerDanmakuBlocktop = 6;
    optional bool playerDanmakuBlockscroll = 7;
    optional bool playerDanmakuBlockbottom = 8;
    optional bool playerDanmakuBlockcolorful = 9;
    optional bool playerDanmakuBlockrepeat = 10;
    optional bool playerDanmakuBlockspecial = 11;
    optional float playerDanmakuOpacity = 12;
    optional float playerDanmakuScalingfactor = 13;
    optional float playerDanmakuDomain = 14;
    optional int32 playerDanmakuSpeed = 15;
    optional bool inlinePlayerDanmakuSwitch = 16;
}

message DanmuPlayerConfig {
    optional bool playerDanmakuSwitchSave = 1;
    optional bool playerDanmakuSwitch = 2;
    optional bool playerDanmakuUseDefaultConfig = 3;
    optional bool playerDanmakuAiRecommendedSwitch = 4;
    optional int32 playerDanmakuAiRecommendedLevel = 5;
    optional bool playerDanmakuBlocktop = 6;
    optional bool playerDanmakuBlockscroll = 7;
    optional bool playerDanmakuBlockbottom = 8;
    optional bool playerDanmakuBlockcolorful = 9;
    optional bool playerDanmakuBlockrepeat = 10;
    optional bool playerDanmakuBlockspecial = 11;
    optional float playerDanmakuOpacity = 12;
    optional float playerDanmakuScalingfactor = 13;
    optional float playerDanmakuDomain = 14;
    optional int32 playerDanmakuSpeed = 15;
    optional bool playerDanmakuEnableblocklist = 16;
    optional bool inlinePlayerDanmakuSwitch = 17;
    optional int32 inlinePlayerDanmakuConfig = 18;
}

message DanmuPlayerDynamicConfig {
    optional int32 progress = 1;
    optional float playerDanmakuDomain = 14;
}

================================================
FILE: BBDown.Core/APP/Response/playviewreply.proto
================================================
syntax = "proto2";
option csharp_namespace = "BBDown.Core.Protobuf";

message VideoInfo {
    optional uint32 quality = 1;
    optional string format = 2;
    optional uint64 timelength = 3;
    optional uint32 videoCodecid = 4;
    repeated StreamItem streamList = 5;
    repeated DashItem dashAudio = 6;
    //杜比伴音流
    optional DolbyItem dolby = 7;
    optional DolbyItem flac = 9;
}

//杜比音频信息
message DolbyItem {
	optional int32 type = 1;
	//音频流
	optional DashItem audio = 2;
}

message PlayAbilityConf {
    optional bool backgroundPlayDisable = 1;
    optional bool flipDisable = 2;
    optional bool castDisable = 3;
    optional bool feedbackDisable = 4;
    optional bool subtitleDisable = 5;
    optional bool playbackRateDisable = 6;
    optional bool timeUpDisable = 7;
    optional bool playbackModeDisable = 8;
    optional bool scaleModeDisable = 9;
    optional bool likeDisable = 10;
    optional bool dislikeDisable = 11;
    optional bool coinDisable = 12;
    optional bool elecDisable = 13;
    optional bool shareDisable = 14;
    optional bool screenShotDisable = 15;
    optional bool lockScreenDisable = 16;
    optional bool recommendDisable = 17;
    optional bool playbackSpeedDisable = 18;
    optional bool definitionDisable = 19;
    optional bool selectionsDisable = 20;
    optional bool nextDisable = 21;
    optional bool editDmDisable = 22;
    optional bool smallWindowDisable = 23;
    optional bool shakeDisable = 24;
}

message ClipInfo {
    optional int32 start = 2;
    optional int32 end = 3;
    optional string toastText = 5;
}

message BusinessInfo {
    optional bool isPreview = 1;
    optional bool bp = 2;
    optional string marlinToken = 3;
    repeated ClipInfo clipInfo = 6;
}

message Event {
    optional Shake shake = 1;
}

message Shake {
    optional string file = 1;
}

message DashItem {
    optional uint32 id = 1;
    optional string baseUrl = 2;
    repeated string backupUrl = 3;
    optional uint32 bandwidth = 4;
    optional uint32 codecid = 5;
    optional string md5 = 6;
    optional uint64 size = 7;
}

message StreamItem {
    optional StreamInfo streamInfo = 1;
    optional DashVideo  dashVideo = 2;
    optional SegmentVideo segmentVideo = 3;
}

message StreamInfo {
    optional uint32 quality = 1;
    optional string format = 2;
    optional string description = 3;
    optional uint32 errCode = 4;
    optional StreamLimit limit = 5;
    optional bool needVip = 6;
    optional bool needLogin = 7;
    optional bool intact = 8;
    optional bool noRexcode = 9;
    optional uint64 attribute = 10;

}

message DashVideo {
    optional string baseUrl = 1;
    repeated string backupUrl = 2;
    optional uint32 bandwidth = 3;
    optional uint32 codecid = 4;
    optional string md5 = 5;
    optional uint64 size = 6;
    optional uint32 audioId = 7;
    optional bool noRexcode = 8;
}

message SegmentVideo {
    repeated ResponseUrl segment = 1;
}

message StreamLimit {
    optional string title = 1;
    optional string uri = 2;
    optional string msg = 3;

}

message ResponseUrl {
    optional uint32 order = 1;
    optional uint64 length = 2;
    optional uint64 size = 3;
    optional string url = 4;
    repeated string backupUrl = 5;
    optional string md5 = 6;
}

message RoleAudioProto {
    // 配音列表
    repeated AudioMaterialProto audioMaterialList = 4;
}

message AudioMaterialProto {
    optional string audioId = 1;
    optional string title = 2;
    optional string edition = 3;
    optional string personName = 5;
    repeated DashItem audio = 7;
}

message PlayDubbingInfo {
    // 背景音频
    optional AudioMaterialProto backgroundAudio = 1;
    // 角色音频列表
    repeated RoleAudioProto roleAudioList = 2;
}

message PlayExtInfo {
    // 播放配音信息
    optional PlayDubbingInfo playDubbingInfo = 1;
}

message PlayViewReply {
    optional VideoInfo videoInfo = 1;
    optional PlayAbilityConf playConf = 2;
    optional BusinessInfo business = 3;
    optional Event event = 4;
    optional PlayExtInfo playExtInfo = 7;
}

================================================
FILE: BBDown.Core/AppHelper.cs
================================================
using BBDown.Core.Protobuf;
using Google.Protobuf;
using System.Buffers.Binary;
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Serialization;
using static BBDown.Core.Util.HTTPUtil;
using static BBDown.Core.Logger;

namespace BBDown.Core;

static class AppHelper
{
    private static readonly string API = "https://grpc.biliapi.net/bilibili.app.playurl.v1.PlayURL/PlayView";
    private static readonly string API2 = "https://app.bilibili.com/bilibili.pgc.gateway.player.v2.PlayURL/PlayView";
    private static readonly string dalvikVer = "2.1.0";
    private static readonly string osVer = "11";
    private static readonly string brand = "M2012K11AC";
    private static readonly string model = "Build/RKQ1.200826.002";
    private static readonly string appVer = "7.32.0";
    private static readonly int build = 7320200; // 新版才能抓到配音
    private static readonly string channel = "xiaomi_cn_tv.danmaku.bili_zm20200902";
    private static readonly Network.Types.TYPE networkType = Network.Types.TYPE.Wifi;
    private static readonly string networkOid = "46007";
    private static readonly string cronet = "1.36.1";
    private static readonly string buvid = "";
    private static readonly string mobiApp = "android";
    private static readonly string appKey = "android64";
    private static readonly string sessionId = "dedf8669";
    private static readonly string platform = "android";
    private static readonly string env = "prod";
    private static readonly int appId = 1;
    private static readonly string region = "CN";
    private static readonly string language = "zh";

    private static PlayViewReq.Types.CodeType GetVideoCodeType(string code)
    {
        return code switch
        {
            "AVC" => PlayViewReq.Types.CodeType.Code264,
            "HEVC" => PlayViewReq.Types.CodeType.Code265,
            "AV1" => PlayViewReq.Types.CodeType.Codeav1,
            _ => PlayViewReq.Types.CodeType.Code265
        };
    }

    /// <summary>
    /// 发起请求并返回响应报文(protobuf -> json)
    /// </summary>
    /// <param name="epId"></param>
    /// <param name="cid"></param>
    /// <param name="qn"></param>
    /// <param name="appkey"></param>
    /// <returns></returns>
    public static async Task<string> DoReqAsync(string aid, string cid, string epId, string qn, bool bangumi, string encoding, string appkey = "")
    {

        var headers = GetHeader(appkey);
        LogDebug("App-Req-Headers: {0}", JsonSerializer.Serialize(headers, JsonContext.Default.DictionaryStringString));
        byte[] data;
        // 只有pgc接口才有配音和片头尾信息
        if (bangumi)
        {
            if (!(string.IsNullOrEmpty(encoding) || encoding == "HEVC"))
                LogWarn("APP的番剧不支持 HEVC 以外的编码");
            var body = GetPayload(Convert.ToInt64(epId), Convert.ToInt64(cid), Convert.ToInt64(qn), PlayViewReq.Types.CodeType.Code265);
            data = await GetPostResponseAsync(API2, body, headers);
        }
        else
        {
            var body = GetPayload(Convert.ToInt64(aid), Convert.ToInt64(cid), Convert.ToInt64(qn), GetVideoCodeType(encoding));
            data = await GetPostResponseAsync(API, body, headers);
        }
        var resp = new MessageParser<PlayViewReply>(() => new PlayViewReply()).ParseFrom(ReadMessage(data));

        LogDebug("PlayViewReplyPlain: {0}", JsonSerializer.Serialize(resp, JsonContext.Default.PlayViewReply));
        return ConvertToDashJson(resp);
    }

    /// <summary>
    /// 将protobuf转换成网页那种json 这样就不用修改之前的解析逻辑了
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    private static string ConvertToDashJson(object data)
    {
        var resp = (PlayViewReply)data;
        var videos = new List<object>();
        var audios = new List<object>();
        var clips = new List<object>();

        if (resp.VideoInfo.StreamList != null)
        {
            foreach (var item in resp.VideoInfo.StreamList)
            {
                if (item.DashVideo != null)
                {
                    videos.Add(new AudioInfoWitCodecId(
                        item.StreamInfo.Quality,
                        item.DashVideo.BaseUrl,
                        item.DashVideo.BackupUrl.ToList(),
                        (uint)(item.DashVideo.Size * 8 / (resp.VideoInfo.Timelength / 1000)),
                        item.DashVideo.Codecid
                    ));
                }
            }
        }

        if (resp.VideoInfo.DashAudio != null)
        {
            audios.AddRange(resp.VideoInfo.DashAudio.Select(item => new AudioInfoWithCodecName(
                item.Id,
                item.BaseUrl,
                item.BackupUrl.ToList(),
                item.Bandwidth,
                "M4A"
            )));
        }

        if (resp.VideoInfo.Flac != null && resp.VideoInfo.Flac.Audio != null)
        {
            audios.Add(new AudioInfoWithCodecName(
                resp.VideoInfo.Flac.Audio.Id,
                resp.VideoInfo.Flac.Audio.BaseUrl,
                resp.VideoInfo.Flac.Audio.BackupUrl.ToList(),
                resp.VideoInfo.Flac.Audio.Bandwidth,
                "FLAC"
            ));
        }

        if (resp.VideoInfo.Dolby != null && resp.VideoInfo.Dolby.Audio != null)
        {
            audios.Add(new AudioInfoWithCodecName(
                resp.VideoInfo.Dolby.Audio.Id,
                resp.VideoInfo.Dolby.Audio.BaseUrl,
                resp.VideoInfo.Dolby.Audio.BackupUrl.ToList(),
                resp.VideoInfo.Dolby.Audio.Bandwidth,
                "E-AC-3"
            ));
        }

        if (resp.Business != null && resp.Business.ClipInfo != null)
        {
            clips.AddRange(resp.Business.ClipInfo.Select(clip => new DashClip(
                clip.Start,
                clip.End,
                clip.ToastText
            )));
        }

        var backgroundAudios = new List<object>();
        var roles = new List<object>();
        if (resp.PlayExtInfo != null && resp.PlayExtInfo.PlayDubbingInfo != null && resp.PlayExtInfo.PlayDubbingInfo.BackgroundAudio != null)
        {
            var dubInfo = resp.PlayExtInfo.PlayDubbingInfo;

            backgroundAudios.AddRange(dubInfo.BackgroundAudio.Audio.Select(item => new AudioInfoWithCodecName(
                item.Id,
                item.BaseUrl,
                item.BackupUrl.ToList(),
                item.Bandwidth,
                "M4A"
            )));

            foreach (var item in dubInfo.RoleAudioList)
            {
                foreach (var role in item.AudioMaterialList)
                {
                    List<object> roleAudios = role.Audio.Select(item => new AudioInfoWithCodecName(
                        item.Id,
                        item.BaseUrl,
                        item.BackupUrl.ToList(),
                        item.Bandwidth,
                        "M4A"
                    )).Cast<object>().ToList();

                    roles.Add(new AudioMaterial(
                        role.AudioId,
                        role.Title ?? role.AudioId,
                        role.PersonName ?? role.Edition ?? "",
                        roleAudios
                    ));
                }
            }
        }

        var json = new DashJson(
            0,
            "0",
            1,
            new DashData(
                resp.VideoInfo.Timelength,
                new DashInfo(
                    videos,
                    audios
                ),
                clips
            ),
            new DubbingInfo(
                backgroundAudios,
                roles
            )
        );

        return JsonSerializer.Serialize(json, JsonContext.Default.DashJson);
    }

    private static byte[] GetPayload(long aid, long cid, long qn, PlayViewReq.Types.CodeType codec)
    {
        var obj = new PlayViewReq
        {
            EpId = aid,
            Cid = cid,
            //obj.Qn = qn;
            Qn = 127,
            Fnval = 4048,
            Fourk = true,
            Spmid = "main.ugc-video-detail.0.0",
            FromSpmid = "main.my-history.0.0",
            PreferCodecType = codec,
            Download = 0, //0:播放 1:flv下载 2:dash下载
            ForceHost = 2 //0:允许使用ip 1:使用http 2:使用https
        };
        LogDebug("PayLoadPlain: {0}", JsonSerializer.Serialize(obj, JsonContext.Default.PlayViewReq));
        return PackMessage(obj.ToByteArray());
    }


    #region 生成Headers相关方法

    private static Dictionary<string, string> GetHeader(string appkey)
    {
        return new Dictionary<string, string>()
        {
            ["Host"] = "grpc.biliapi.net",
            ["user-agent"] = $"Dalvik/{dalvikVer} (Linux; U; Android {osVer}; {brand} {model}) {appVer} os/android model/{brand} mobi_app/android build/{build} channel/{channel} innerVer/{build} osVer/{osVer} network/2 grpc-java-cronet/{cronet}",
            ["te"] = "trailers",
            ["x-bili-fawkes-req-bin"] = GenerateFawkesReqBin(),
            ["x-bili-metadata-bin"] = GenerateMetadataBin(appkey),
            ["authorization"] = $"identify_v1 {Config.TOKEN}",
            ["x-bili-device-bin"] = GenerateDeviceBin(),
            ["x-bili-network-bin"] = GenerateNetworkBin(),
            ["x-bili-restriction-bin"] = "",
            ["x-bili-locale-bin"] = GenerateLocaleBin(),
            ["x-bili-exps-bin"] = "",
            ["grpc-encoding"] = "gzip",
            ["grpc-accept-encoding"] = "identity,gzip",
            ["grpc-timeout"] = "17996161u",
        };
    }

    private static string GenerateLocaleBin()
    {
        var obj = new Locale
        {
            CLocale = new Locale.Types.LocaleIds
            {
                Language = language,
                Region = region
            }
        };
        return Convert.ToBase64String(obj.ToByteArray());
    }

    private static string GenerateNetworkBin()
    {
        var obj = new Network
        {
            Type = networkType,
            Oid = networkOid
        };
        return Convert.ToBase64String(obj.ToByteArray());
    }

    private static string GenerateDeviceBin()
    {
        var obj = new Device
        {
            AppId = appId,
            Build = build,
            Buvid = buvid,
            MobiApp = mobiApp,
            Platform = platform,
            Channel = channel,
            Brand = brand,
            Model = model,
            Osver = osVer
        };
        return Convert.ToBase64String(obj.ToByteArray());
    }

    private static string GenerateMetadataBin(string appkey)
    {
        var obj = new Metadata
        {
            AccessKey = appkey,
            MobiApp = mobiApp,
            Build = build,
            Channel = channel,
            Buvid = buvid,
            Platform = platform
        };
        return Convert.ToBase64String(obj.ToByteArray());
    }

    private static string GenerateFawkesReqBin()
    {
        var obj = new FawkesReq
        {
            Appkey = appKey,
            Env = env,
            SessionId = sessionId
        };
        return Convert.ToBase64String(obj.ToByteArray());
    }

    #endregion

    /// <summary>
    /// 读取gRPC响应流 通过前5字节信息 解析/解压后面的报文体
    /// </summary>
    /// <param name="data"></param>
    /// <returns>字节流</returns>
    public static byte[] ReadMessage(byte[] data)
    {
        byte first;
        int size;
        (first, size) = ReadInfo(data);
        return first == 1 ? GzipDecompress(data[5..]) : data[5..(5 + size)];
    }

    /// <summary>
    /// 读取报文长度
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    private static (byte first, int size) ReadInfo(byte[] data)
    {
        var value1 = data[0];
        var value2 = data[1..5];

        return (value1, BinaryPrimitives.ReadInt32BigEndian(value2));
    }

    /// <summary>
    /// 给请求载荷添加头部信息
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public static byte[] PackMessage(byte[] input)
    {
        using var stream = new MemoryStream();
        using (var writer = new BinaryWriter(stream))
        {
            var comp = GzipCompress(input);
            var reverse = (stackalloc byte[4]);
            writer.Write((byte)1);
            BinaryPrimitives.WriteInt32BigEndian(reverse, comp.Length);
            writer.Write(reverse);
            writer.Write(comp);
        }
        return stream.ToArray();
    }

    /// <summary>
    /// gzip压缩
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    private static byte[] GzipCompress(byte[] data)
    {
        using var output = new MemoryStream();
        using (var comp = new GZipStream(output, CompressionMode.Compress))
        {
            comp.Write(data, 0, data.Length);
        }
        return output.ToArray();
    }

    /// <summary>
    /// gzip解压
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    private static byte[] GzipDecompress(byte[] data)
    {
        using var output = new MemoryStream();
        using (var input = new MemoryStream(data))
        {
            using var decomp = new GZipStream(input, CompressionMode.Decompress);
            decomp.CopyTo(output);
        }
        return output.ToArray();
    }
}


[JsonSerializable(typeof(AudioMaterial))]
[JsonSerializable(typeof(DubbingInfo))]
[JsonSerializable(typeof(DashClip))]
[JsonSerializable(typeof(AudioInfoWithCodecName))]
[JsonSerializable(typeof(AudioInfoWitCodecId))]
[JsonSerializable(typeof(DashJson))]
[JsonSerializable(typeof(PlayViewReq))]
[JsonSerializable(typeof(PlayViewReply))]
[JsonSerializable(typeof(Dictionary<string, string>))]
internal partial class JsonContext : JsonSerializerContext { }

internal class AudioMaterial
{
    [JsonPropertyName("audio_id")]
    public string AudioId { get; }
    [JsonPropertyName("title")]
    public string Title { get; }
    [JsonPropertyName("person_name")]
    public string PersonName { get; }
    [JsonPropertyName("audio")]
    public List<object> Audio { get; }

    public AudioMaterial(string audio_id, string title, string person_name, List<object> audio)
    {
        AudioId = audio_id;
        Title = title;
        PersonName = person_name;
        Audio = audio;
    }

    public override bool Equals(object? obj) => obj is AudioMaterial other && AudioId == other.AudioId && Title == other.Title && PersonName == other.PersonName && Audio == other.Audio;
    public override int GetHashCode() => HashCode.Combine(Title, Audio);
}

internal class DubbingInfo
{
    [JsonPropertyName("background_audio")]
    public List<object> BackgroundAudio { get; }
    [JsonPropertyName("role_audio_list")]
    public List<object> RoleAudioList { get; }

    public DubbingInfo(List<object> background_audio, List<object> role_audio_list)
    {
        BackgroundAudio = background_audio;
        RoleAudioList = role_audio_list;
    }

    public override bool Equals(object? obj) => obj is DubbingInfo other && BackgroundAudio == other.BackgroundAudio && RoleAudioList == other.RoleAudioList;
    public override int GetHashCode() => HashCode.Combine(BackgroundAudio, RoleAudioList);
}

internal class DashClip
{
    [JsonPropertyName("start")]
    public int Start { get; }
    [JsonPropertyName("end")]
    public int End { get; }
    [JsonPropertyName("toastText")]
    public string ToastText { get; }

    public DashClip(int start, int end, string toastText)
    {
        Start = start;
        End = end;
        ToastText = toastText;
    }

    public override bool Equals(object? obj) => obj is DashClip other && Start == other.Start && End == other.End && ToastText == other.ToastText;
    public override int GetHashCode() => HashCode.Combine(Start, End, ToastText);
}

internal class AudioInfoWithCodecName
{
    [JsonPropertyName("id")]
    public uint Id { get; }
    [JsonPropertyName("base_url")]
    public string BaseUrl { get; }
    [JsonPropertyName("backup_url")]
    public List<string> BackupUrl { get; }
    [JsonPropertyName("bandwidth")]
    public uint Bandwidth { get; }
    [JsonPropertyName("codecs")]
    public string Codecs { get; }

    public AudioInfoWithCodecName(uint id, string base_url, List<string> backup_url, uint bandwidth, string codecs)
    {
        Id = id;
        BaseUrl = base_url;
        BackupUrl = backup_url;
        Bandwidth = bandwidth;
        Codecs = codecs;
    }

    public override bool Equals(object? obj) => obj is AudioInfoWithCodecName other && Id == other.Id && BaseUrl == other.BaseUrl && BackupUrl.SequenceEqual(other.BackupUrl) && Bandwidth == other.Bandwidth && Codecs == other.Codecs;
    public override int GetHashCode() => HashCode.Combine(Id, BaseUrl, BackupUrl, Bandwidth, Codecs);
}

internal class AudioInfoWitCodecId
{
    [JsonPropertyName("id")]
    public uint Id { get; }
    [JsonPropertyName("base_url")]
    public string BaseUrl { get; }
    [JsonPropertyName("backup_url")]
    public List<string> BackupUrl { get; }
    [JsonPropertyName("bandwidth")]
    public uint Bandwidth { get; }
    [JsonPropertyName("codecid")]
    public uint Codecid { get; }

    public AudioInfoWitCodecId(uint id, string base_url, List<string> backup_url, uint bandwidth, uint codecid)
    {
        Id = id;
        BaseUrl = base_url;
        BackupUrl = backup_url;
        Bandwidth = bandwidth;
        Codecid = codecid;
    }

    public override bool Equals(object? obj) => obj is AudioInfoWitCodecId other && Id == other.Id && BaseUrl == other.BaseUrl && Bandwidth == other.Bandwidth && Codecid == other.Codecid;
    public override int GetHashCode() => HashCode.Combine(Id, BaseUrl, Bandwidth, Codecid);
}

internal class DashInfo
{
    [JsonPropertyName("video")]
    public List<object> Video { get; }
    [JsonPropertyName("audio")]
    public List<object> Audio { get; }

    public DashInfo(List<object> video, List<object> audio)
    {
        Video = video;
        Audio = audio;
    }

    public override bool Equals(object? obj) => obj is DashInfo other && Equ
Download .txt
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
Download .txt
SYMBOL INDEX (281 symbols across 36 files)

FILE: BBDown.Core/AppHelper.cs
  class AppHelper (line 12) | static class AppHelper
    method GetVideoCodeType (line 36) | private static PlayViewReq.Types.CodeType GetVideoCodeType(string code)
    method DoReqAsync (line 55) | public static async Task<string> DoReqAsync(string aid, string cid, st...
    method ConvertToDashJson (line 85) | private static string ConvertToDashJson(object data)
    method GetPayload (line 208) | private static byte[] GetPayload(long aid, long cid, long qn, PlayView...
    method GetHeader (line 231) | private static Dictionary<string, string> GetHeader(string appkey)
    method GenerateLocaleBin (line 252) | private static string GenerateLocaleBin()
    method GenerateNetworkBin (line 265) | private static string GenerateNetworkBin()
    method GenerateDeviceBin (line 275) | private static string GenerateDeviceBin()
    method GenerateMetadataBin (line 292) | private static string GenerateMetadataBin(string appkey)
    method GenerateFawkesReqBin (line 306) | private static string GenerateFawkesReqBin()
    method ReadMessage (line 324) | public static byte[] ReadMessage(byte[] data)
    method ReadInfo (line 337) | private static (byte first, int size) ReadInfo(byte[] data)
    method PackMessage (line 350) | public static byte[] PackMessage(byte[] input)
    method GzipCompress (line 370) | private static byte[] GzipCompress(byte[] data)
    method GzipDecompress (line 385) | private static byte[] GzipDecompress(byte[] data)
  class JsonContext (line 398) | [JsonSerializable(typeof(AudioMaterial))]
  class AudioMaterial (line 409) | internal class AudioMaterial
    method AudioMaterial (line 420) | public AudioMaterial(string audio_id, string title, string person_name...
    method Equals (line 428) | public override bool Equals(object? obj) => obj is AudioMaterial other...
    method GetHashCode (line 429) | public override int GetHashCode() => HashCode.Combine(Title, Audio);
  class DubbingInfo (line 432) | internal class DubbingInfo
    method DubbingInfo (line 439) | public DubbingInfo(List<object> background_audio, List<object> role_au...
    method Equals (line 445) | public override bool Equals(object? obj) => obj is DubbingInfo other &...
    method GetHashCode (line 446) | public override int GetHashCode() => HashCode.Combine(BackgroundAudio,...
  class DashClip (line 449) | internal class DashClip
    method DashClip (line 458) | public DashClip(int start, int end, string toastText)
    method Equals (line 465) | public override bool Equals(object? obj) => obj is DashClip other && S...
    method GetHashCode (line 466) | public override int GetHashCode() => HashCode.Combine(Start, End, Toas...
  class AudioInfoWithCodecName (line 469) | internal class AudioInfoWithCodecName
    method AudioInfoWithCodecName (line 482) | public AudioInfoWithCodecName(uint id, string base_url, List<string> b...
    method Equals (line 491) | public override bool Equals(object? obj) => obj is AudioInfoWithCodecN...
    method GetHashCode (line 492) | public override int GetHashCode() => HashCode.Combine(Id, BaseUrl, Bac...
  class AudioInfoWitCodecId (line 495) | internal class AudioInfoWitCodecId
    method AudioInfoWitCodecId (line 508) | public AudioInfoWitCodecId(uint id, string base_url, List<string> back...
    method Equals (line 517) | public override bool Equals(object? obj) => obj is AudioInfoWitCodecId...
    method GetHashCode (line 518) | public override int GetHashCode() => HashCode.Combine(Id, BaseUrl, Ban...
  class DashInfo (line 521) | internal class DashInfo
    method DashInfo (line 528) | public DashInfo(List<object> video, List<object> audio)
    method Equals (line 534) | public override bool Equals(object? obj) => obj is DashInfo other && E...
    method GetHashCode (line 535) | public override int GetHashCode() => HashCode.Combine(Video, Audio);
  class DashData (line 538) | internal class DashData
    method DashData (line 547) | public DashData(ulong timelength, DashInfo dash, List<object> clipList)
    method Equals (line 554) | public override bool Equals(object? obj) => obj is DashData other && T...
    method GetHashCode (line 555) | public override int GetHashCode() => HashCode.Combine(TimeLength, Dash...
  class DashJson (line 558) | internal class DashJson
    method DashJson (line 571) | public DashJson(int code, string message, int ttl, DashData data, Dubb...
    method Equals (line 580) | public override bool Equals(object? obj) => obj is DashJson other && C...
    method GetHashCode (line 581) | public override int GetHashCode() => HashCode.Combine(Code, Message, T...

FILE: BBDown.Core/Config.cs
  class Config (line 3) | public static class Config

FILE: BBDown.Core/DanmakuUtil.cs
  class DanmakuUtil (line 7) | public static class DanmakuUtil
    method ParseXml (line 23) | public static DanmakuItem[]? ParseXml(string xmlPath)
    method SaveAsAssAsync (line 77) | public static async Task SaveAsAssAsync(DanmakuItem[] danmakus, string...
    class PositionController (line 120) | protected class PositionController
      method PositionController (line 129) | public PositionController()
      method UpdatePosition (line 139) | public int UpdatePosition(int type, double time, int length)
    class DanmakuItem (line 169) | public class DanmakuItem
      method DanmakuItem (line 171) | public DanmakuItem(string[] attrs, string content)
      method ComputeTime (line 203) | private static string ComputeTime(double second)
    class DanmakuComparer (line 228) | public class DanmakuComparer : IComparer<DanmakuItem>
      method Compare (line 230) | public int Compare(DanmakuItem? x, DanmakuItem? y)

FILE: BBDown.Core/Entity/Entity.cs
  class Entity (line 6) | public static class Entity
    class Page (line 8) | public class Page
      method Page (line 28) | [SetsRequiredMembers]
      method Page (line 41) | [SetsRequiredMembers]
      method Page (line 55) | [SetsRequiredMembers]
      method Page (line 70) | [SetsRequiredMembers]
      method Page (line 87) | [SetsRequiredMembers]
      method Equals (line 103) | public override bool Equals(object? obj)
      method GetHashCode (line 111) | public override int GetHashCode()
    class ViewPoint (line 117) | public class ViewPoint
    class Video (line 124) | public class Video
      method Equals (line 136) | public override bool Equals(object? obj)
      method GetHashCode (line 148) | public override int GetHashCode()
    class Audio (line 154) | public class Audio
      method Equals (line 166) | public override bool Equals(object? obj)
      method GetHashCode (line 176) | public override int GetHashCode()
    class Subtitle (line 182) | public class Subtitle
    class Clip (line 189) | public class Clip
    class AudioMaterial (line 196) | public class AudioMaterial
      method AudioMaterial (line 202) | [SetsRequiredMembers]
      method AudioMaterial (line 210) | [SetsRequiredMembers]
    class AudioMaterialInfo (line 219) | public class AudioMaterialInfo

FILE: BBDown.Core/Entity/ParsedResult.cs
  class ParsedResult (line 5) | public class ParsedResult

FILE: BBDown.Core/Entity/VInfo.cs
  class VInfo (line 5) | public class VInfo

FILE: BBDown.Core/Fetcher/BangumiInfoFetcher.cs
  class BangumiInfoFetcher (line 8) | public class BangumiInfoFetcher : IFetcher
    method FetchAsync (line 10) | public async Task<VInfo> FetchAsync(string id)

FILE: BBDown.Core/Fetcher/CheeseInfoFetcher.cs
  class CheeseInfoFetcher (line 8) | public class CheeseInfoFetcher : IFetcher
    method FetchAsync (line 10) | public async Task<VInfo> FetchAsync(string id)

FILE: BBDown.Core/Fetcher/FavListFetcher.cs
  class FavListFetcher (line 14) | public class FavListFetcher : IFetcher
    method FetchAsync (line 16) | public async Task<VInfo> FetchAsync(string id)

FILE: BBDown.Core/Fetcher/IntlBangumiInfoFetcher.cs
  class IntlBangumiInfoFetcher (line 9) | public partial class IntlBangumiInfoFetcher : IFetcher
    method FetchAsync (line 11) | public async Task<VInfo> FetchAsync(string id)
    method StateRegex (line 124) | [GeneratedRegex("window.__INITIAL_STATE__=([\\s\\S].*?);\\(function\\(...

FILE: BBDown.Core/Fetcher/MediaListFetcher.cs
  class MediaListFetcher (line 13) | public class MediaListFetcher : IFetcher
    method FetchAsync (line 15) | public async Task<VInfo> FetchAsync(string id)

FILE: BBDown.Core/Fetcher/NormalInfoFetcher.cs
  class NormalInfoFetcher (line 10) | public partial class NormalInfoFetcher : IFetcher
    method FetchAsync (line 12) | public async Task<VInfo> FetchAsync(string id)
    method EpIdRegex (line 129) | [GeneratedRegex("ep(\\d+)")]

FILE: BBDown.Core/Fetcher/SeriesListFetcher.cs
  class SeriesListFetcher (line 12) | public class SeriesListFetcher : IFetcher
    method FetchAsync (line 14) | public async Task<VInfo> FetchAsync(string id)

FILE: BBDown.Core/Fetcher/SpaceVideoFetcher.cs
  class SpaceVideoFetcher (line 8) | public class SpaceVideoFetcher : IFetcher
    method FetchAsync (line 10) | public async Task<VInfo> FetchAsync(string id)
    method GetVideosByPageAsync (line 45) | static async Task<List<string>> GetVideosByPageAsync(int pageNumber, i...
    method GetValidFileName (line 60) | private static string GetValidFileName(string input, string re = ".", ...

FILE: BBDown.Core/FetcherFactory.cs
  class FetcherFactory (line 5) | public static class FetcherFactory
    method CreateFetcher (line 12) | public static IFetcher CreateFetcher(string aidOri, bool useIntlApi)

FILE: BBDown.Core/IFetcher.cs
  type IFetcher (line 3) | public interface IFetcher
    method FetchAsync (line 5) | Task<Entity.VInfo> FetchAsync(string id);

FILE: BBDown.Core/Logger.cs
  class Logger (line 3) | public static class Logger
    method Log (line 5) | public static void Log(object text, bool enter = true)
    method LogError (line 11) | public static void LogError(object text)
    method LogColor (line 20) | public static void LogColor(object text, bool time = true)
    method LogWarn (line 33) | public static void LogWarn(object text, bool time = true)
    method LogDebug (line 46) | public static void LogDebug(string toFormat, params object[] args)

FILE: BBDown.Core/Parser.cs
  class Parser (line 12) | public static partial class Parser
    method WbiSign (line 14) | public static string WbiSign(string api)
    method GetPlayJsonAsync (line 19) | private static async Task<string> GetPlayJsonAsync(string encoding, st...
    method GetPlayJsonAsync (line 76) | private static async Task<string> GetPlayJsonAsync(string aid, string ...
    method ExtractTracksAsync (line 96) | public static async Task<ParsedResult> ExtractTracksAsync(string aidOr...
    method GetVideoCodec (line 436) | private static string GetVideoCodec(string code)
    method GetMaxQn (line 447) | private static string GetMaxQn()
    method GetTimeStamp (line 452) | private static string GetTimeStamp(bool bflag)
    method GetSign (line 458) | private static string GetSign(string parms, bool isBiliPlus)
    method PlayerJsonRegex (line 464) | [GeneratedRegex("window.__playinfo__=([\\s\\S]*?)<\\/script>")]
    method BaseUrlRegex (line 466) | [GeneratedRegex("http.*:\\d+")]

FILE: BBDown.Core/Util/BilibiliBvConverter.cs
  class BilibiliBvConverter (line 6) | public static class BilibiliBvConverter
    method BilibiliBvConverter (line 21) | static BilibiliBvConverter()
    method Encode (line 29) | public static string Encode(long avid)
    method Decode (line 55) | public static long Decode(string bvid_str)

FILE: BBDown.Core/Util/HTTPUtil.cs
  class HTTPUtil (line 7) | public static class HTTPUtil
    method RandomVersion (line 23) | private static string RandomVersion(int min, int max)
    method GetRandomUserAgent (line 29) | private static string GetRandomUserAgent()
    method GetWebSourceAsync (line 37) | public static async Task<string> GetWebSourceAsync(string url, string?...
    method GetWebLocationAsync (line 59) | public static async Task<string> GetWebLocationAsync(string url)
    method GetPostResponseAsync (line 74) | public static async Task<byte[]> GetPostResponseAsync(string Url, byte...

FILE: BBDown.Core/Util/SubUtil.cs
  class SubUtil (line 11) | public static partial class SubUtil
    method GetSubtitleCode (line 14) | public static (string, string) GetSubtitleCode(string key)
    method GetIntlSubtitlesFromApi1Async (line 220) | private static async Task<List<Subtitle>?> GetIntlSubtitlesFromApi1Asy...
    method GetIntlSubtitlesFromApi2Async (line 254) | private static async Task<List<Subtitle>?> GetIntlSubtitlesFromApi2Asy...
    method GetSubtitlesFromApi1Async (line 290) | private static async Task<List<Subtitle>?> GetSubtitlesFromApi1Async(s...
    method GetSubtitlesFromApi2Async (line 328) | private static async Task<List<Subtitle>?> GetSubtitlesFromApi2Async(s...
    method GetPayload (line 361) | private static byte[] GetPayload(long aid, long cid)
    method GetSubtitlesFromApi3Async (line 373) | private static async Task<List<Subtitle>?> GetSubtitlesFromApi3Async(s...
    method GetSubtitlesAsync (line 408) | public static async Task<List<Subtitle>> GetSubtitlesAsync(string aid,...
    method SaveSubtitleAsync (line 444) | public static async Task SaveSubtitleAsync(string url, string path)
    method ConvertSubFromJson (line 452) | private static string ConvertSubFromJson(string jsonString)
    method FormatTime (line 477) | private static string FormatTime(double sec) //64.13
    method NonCapsRegex (line 482) | [GeneratedRegex("-[a-z]")]

FILE: BBDown/BBDownApiServer.cs
  class BBDownApiServer (line 19) | public class BBDownApiServer
    method SetUpServer (line 25) | public void SetUpServer()
    method Run (line 96) | public void Run(string url)
    method AddDownloadTaskAsync (line 115) | private async Task<DownloadTask> AddDownloadTaskAsync(MyOption option)
  type DownloadTask (line 158) | public record DownloadTask(string Aid, string Url, long TaskCreateTime)
  type DownloadTaskCollection (line 180) | public record DownloadTaskCollection(List<DownloadTask> Running, List<Do...
  type MyOptionBindingResult (line 182) | record struct MyOptionBindingResult<T>(T? Result, Exception? Exception)
  class AppJsonSerializerContext (line 208) | [JsonSerializable(typeof(ProblemDetails))]
  class SourceGenerationContext (line 219) | [JsonSerializable(typeof(MyOption))]

FILE: BBDown/BBDownAria2c.cs
  class BBDownAria2c (line 7) | static class BBDownAria2c
    method RunCommandCodeAsync (line 11) | public static async Task<int> RunCommandCodeAsync(string command, stri...
    method DownloadFileByAria2cAsync (line 23) | public static async Task DownloadFileByAria2cAsync(string url, string ...

FILE: BBDown/BBDownConfigParser.cs
  class BBDownConfigParser (line 11) | internal static class BBDownConfigParser
    method HandleConfig (line 13) | public static void HandleConfig(List<string> newArgsList, RootCommand ...

FILE: BBDown/BBDownDownloadUtil.cs
  class BBDownDownloadUtil (line 15) | internal static class BBDownDownloadUtil
    class DownloadConfig (line 17) | public class DownloadConfig
    method RangeDownloadToTmpAsync (line 26) | private static async Task RangeDownloadToTmpAsync(int id, string url, ...
    method DownloadFileAsync (line 77) | public static async Task DownloadFileAsync(string url, string path, Do...
    method MultiThreadDownloadFileAsync (line 108) | public static async Task MultiThreadDownloadFileAsync(string url, stri...
    method GetAllClips (line 163) | private static List<Clip> GetAllClips(string url, long fileSize)
    method GetFileSizeAsync (line 196) | private static async Task<long> GetFileSizeAsync(string url)
    method ReplaceUrl (line 215) | private static string ReplaceUrl(string url)

FILE: BBDown/BBDownEnums.cs
  type BBDownDanmakuFormat (line 6) | public enum BBDownDanmakuFormat
  class BBDownDanmakuFormatInfo (line 12) | public static class BBDownDanmakuFormatInfo
    method FromFormatName (line 20) | public static BBDownDanmakuFormat FromFormatName(string formatName)

FILE: BBDown/BBDownLoginUtil.cs
  class BBDownLoginUtil (line 14) | internal static class BBDownLoginUtil
    method GetLoginStatusAsync (line 16) | public static async Task<string> GetLoginStatusAsync(string qrcodeKey)
    method LoginWEB (line 22) | public static async Task LoginWEB()
    method LoginTV (line 78) | public static async Task LoginTV()

FILE: BBDown/BBDownMuxer.cs
  class BBDownMuxer (line 16) | static partial class BBDownMuxer
    method RunExe (line 21) | private static int RunExe(string app, string parms, bool customBin = f...
    method EscapeString (line 43) | private static string EscapeString(string str)
    method MuxByMp4box (line 48) | private static int MuxByMp4box(string url, string videoPath, string au...
    method MuxAV (line 100) | public static int MuxAV(bool useMp4box, string bvid, string videoPath,...
    method MergeFLV (line 205) | public static void MergeFLV(string[] files, string outPath)

FILE: BBDown/BBDownUtil.cs
  class BBDownUtil (line 19) | static partial class BBDownUtil
    method CheckUpdateAsync (line 21) | public static async Task CheckUpdateAsync()
    method GetAvIdAsync (line 41) | public static async Task<string> GetAvIdAsync(string input)
    method FormatFileSize (line 209) | public static string FormatFileSize(double fileSize)
    method FormatTime (line 221) | public static string FormatTime(int time, bool absolute = false)
    method FixAvidAsync (line 241) | private static async Task<string> FixAvidAsync(string avid)
    method GetAidByBV (line 250) | private static string GetAidByBV(string bv)
    method GetEpidBySSIdAsync (line 256) | private static async Task<string> GetEpidBySSIdAsync(string ssid)
    method GetEpIdByBangumiSSIdAsync (line 265) | private static async Task<string> GetEpIdByBangumiSSIdAsync(string ssId)
    method GetEpIdByMDAsync (line 274) | private static async Task<string> GetEpIdByMDAsync(string mdId)
    method CombineMultipleFilesIntoSingleFile (line 288) | public static void CombineMultipleFilesIntoSingleFile(string[] files, ...
    method GetFiles (line 321) | public static string[] GetFiles(string dir, string ext)
    method GetValidFileName (line 341) | public static string GetValidFileName(string input, string re = "_", b...
    method GetQueryString (line 364) | public static string GetQueryString(string name, string url)
    method GetSession (line 379) | public static string GetSession(string buvid3)
    method GetSign (line 385) | public static string GetSign(string parms)
    method GetTimeStamp (line 391) | public static string GetTimeStamp(bool bflag)
    method GetRandomString (line 399) | public static string GetRandomString(int length)
    method ToQueryString (line 407) | public static string ToQueryString(NameValueCollection nameValueCollec...
    method ToDictionary (line 414) | public static Dictionary<string, string> ToDictionary(this NameValueCo...
    method GetTVLoginParms (line 424) | public static NameValueCollection GetTVLoginParms()
    method CheckFFmpegDOVI (line 459) | public static bool CheckFFmpegDOVI()
    method FetchPointsAsync (line 498) | public static async Task<List<ViewPoint>> FetchPointsAsync(string cid,...
    method GetFFmpegMetaString (line 528) | public static string GetFFmpegMetaString(List<ViewPoint> points)
    method GetMp4boxMetaString (line 550) | public static string GetMp4boxMetaString(List<ViewPoint> points)
    method FindExecutable (line 560) | public static string? FindExecutable(string name)
    method RSubString (line 568) | public static string RSubString(string sub)
    method GetMixinKey (line 574) | private static string GetMixinKey(string orig)
    method CheckLogin (line 590) | public static async Task<bool> CheckLogin(string cookie)
    method AvRegex (line 609) | [GeneratedRegex("av(\\d+)")]
    method BVRegex (line 611) | [GeneratedRegex("[Bb][Vv]1(\\w+)")]
    method EpRegex (line 613) | [GeneratedRegex("/ep(\\d+)")]
    method SsRegex (line 615) | [GeneratedRegex("/ss(\\d+)")]
    method UidRegex (line 617) | [GeneratedRegex(@"space\.bilibili\.com/(\d+)")]
    method GlobalEpRegex (line 619) | [GeneratedRegex(@"\.bilibili\.tv\/\w+\/play\/\d+\/(\d+)")]
    method BangumiMdRegex (line 621) | [GeneratedRegex("bangumi/media/(md\\d+)")]
    method StateRegex (line 623) | [GeneratedRegex(@"window.__INITIAL_STATE__=([\s\S].*?);\(function\(\)")]
    method MdRegex (line 625) | [GeneratedRegex("md(\\d+)")]
    method QueryRegex (line 627) | [GeneratedRegex("(^|&)?(\\w+)=([^&]+)(&|$)?", RegexOptions.Compiled)]
    method LibavutilRegex (line 629) | [GeneratedRegex("libavutil\\s+(\\d+)\\. +(\\d+)\\.")]

FILE: BBDown/CommandLineInvoker.cs
  class CommandLineInvoker (line 9) | internal static class CommandLineInvoker
    class MyOptionBinder (line 92) | class MyOptionBinder : BinderBase<MyOption>
      method GetBoundValue (line 94) | protected override MyOption GetBoundValue(BindingContext bindingCont...
    method GetRootCommand (line 162) | public static RootCommand GetRootCommand(Func<MyOption, Task> action)

FILE: BBDown/ConsoleQRCode.cs
  class ConsoleQRCode (line 6) | public class ConsoleQRCode : AbstractQRCode
    method ConsoleQRCode (line 8) | public ConsoleQRCode() { }
    method ConsoleQRCode (line 10) | public ConsoleQRCode(QRCodeData data) : base(data) { }
    method GetGraphic (line 12) | public void GetGraphic() => GetGraphic(ConsoleColor.Black, ConsoleColo...
    method GetGraphic (line 14) | public void GetGraphic(ConsoleColor darkColor, ConsoleColor lightColor)

FILE: BBDown/Model/ServeRequestOptions.cs
  class ServeRequestOptions (line 3) | internal class ServeRequestOptions : MyOption

FILE: BBDown/MyOption.cs
  class MyOption (line 3) | internal class MyOption

FILE: BBDown/Program.Methods.cs
  class Program (line 16) | internal partial class Program
    method HandleDeprecatedOptions (line 23) | private static void HandleDeprecatedOptions(MyOption myOption)
    method ParseEncodingPriority (line 77) | private static Dictionary<string, byte> ParseEncodingPriority(MyOption...
    method ParseDownloadDanmakuFormats (line 102) | private static BBDownDanmakuFormat[] ParseDownloadDanmakuFormats(MyOpt...
    method ParseDfnPriority (line 121) | private static Dictionary<string, int> ParseDfnPriority(MyOption myOpt...
    method FindBinaries (line 143) | private static void FindBinaries(MyOption myOption)
    method HandleConflictingOptions (line 199) | private static void HandleConflictingOptions(MyOption myOption)
    method ChangeWorkingDir (line 222) | private static void ChangeWorkingDir(MyOption myOption)
    method LoadCredentials (line 243) | private static void LoadCredentials(MyOption myOption)
    method SaveAidToFile (line 268) | public static void SaveAidToFile(string aid)
    method CheckAidFromFile (line 278) | public static bool CheckAidFromFile(string aid)
    method GetSelectedPages (line 297) | private static List<string>? GetSelectedPages(MyOption myOption, VInfo...
    method HandlePcdn (line 360) | private static void HandlePcdn(MyOption myOption, Video? selectedVideo...
    method PrintAllTracksInfo (line 412) | private static void PrintAllTracksInfo(ParsedResult parsedResult, int ...
    method PrintSelectedTrackInfo (line 457) | private static void PrintSelectedTrackInfo(Video? selectedVideo, Audio...
    method SelectTrackManually (line 478) | private static void SelectTrackManually(ParsedResult parsedResult, ref...
    method DownloadTrackAsync (line 502) | private static async Task DownloadTrackAsync(string url, string destPa...
    method PcdnRegex (line 523) | [GeneratedRegex("://.*:\\d+/")]
    method AkamRegex (line 525) | [GeneratedRegex("://.*akamaized\\.net/")]
    method UposRegex (line 527) | [GeneratedRegex("://[^/]+/")]

FILE: BBDown/Program.cs
  class Program (line 25) | partial class Program
    method FormatTimeStamp (line 33) | private static string FormatTimeStamp(long ts, string format)
    class MyOptionJsonContext (line 46) | [JsonSerializable(typeof(MyOption))]
    method Console_CancelKeyPress (line 50) | private static void Console_CancelKeyPress(object? sender, ConsoleCanc...
    method Main (line 64) | public static async Task<int> Main(params string[] args)
    method RunApp (line 164) | private static Task RunApp(MyOption myOption)
    method StartServer (line 171) | private static void StartServer(string? listenUrl)
    method SetUpWork (line 181) | public static (Dictionary<string, byte> encodingPriority, Dictionary<s...
    method GetVideoInfoAsync (line 225) | public static async Task<(string fetchedAid, VInfo vInfo, string apiTy...
    method DownloadPagesAsync (line 324) | public static async Task DownloadPagesAsync(MyOption myOption, VInfo v...
    method DownloadPageAsync (line 381) | private static async Task DownloadPageAsync(Page p, MyOption myOption,...
    method DoWorkAsync (line 797) | private static async Task DoWorkAsync(MyOption myOption)
    method SortTracks (line 820) | private static List<Video> SortTracks(List<Video> videoTracks, Diction...
    method SortTracks (line 838) | private static List<Audio> SortTracks(List<Audio> audioTracks, Diction...
    method FormatSavePath (line 846) | private static string FormatSavePath(string savePathFormat, string tit...
    method InfoRegex (line 896) | [GeneratedRegex("<([\\w:\\-.]+?)>")]

FILE: BBDown/ProgressBar.cs
  class ProgressBar (line 10) | class ProgressBar : IDisposable, IProgress<double>
    method ProgressBar (line 33) | public ProgressBar(DownloadTask? task = null)
    method Report (line 52) | public void Report(double value)
    method Report (line 59) | public void Report(double value, long bytesCount)
    method SpeedTimerHandler (line 67) | private void SpeedTimerHandler(object? state)
    method TimerHandler (line 89) | private void TimerHandler(object? state)
    method UpdateText (line 111) | private void UpdateText(string text)
    method ResetTimer (line 142) | private void ResetTimer()
    method ResetSpeedTimer (line 147) | private void ResetSpeedTimer()
    method Dispose (line 152) | public void Dispose()
Condensed preview — 60 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (335K chars).
[
  {
    "path": ".dockerignore",
    "chars": 6361,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": ".editorconfig",
    "chars": 249,
    "preview": "# EditorConfig is awesome: https://EditorConfig.org\r\n\r\n# top-most EditorConfig file\r\nroot = true\r\n\r\n[*]\r\nindent_style = "
  },
  {
    "path": ".github/issue_template.md",
    "chars": 272,
    "preview": "<!-- 提问前请确认你的版本是最新版 不要拿一个过时的版本来咨询为什么某某功能失效 -->\n#### 1. 你使用的BBDown版本是什么?(指明 Release / Actions / DotnetTool)\n。。。\n\n#### 2. "
  },
  {
    "path": ".github/workflows/build_latest.yml",
    "chars": 5803,
    "preview": "name: Build Latest\r\n\r\non: [push,workflow_dispatch]\r\n\r\nenv:\r\n  DOTNET_SDK_VERSION: \"9.0.306\"\r\n  ACTIONS_ALLOW_USE_UNSECUR"
  },
  {
    "path": ".gitignore",
    "chars": 6361,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": "BBDown/BBDown.csproj",
    "chars": 929,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk.Web\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net9.0</T"
  },
  {
    "path": "BBDown/BBDownApiServer.cs",
    "chars": 8998,
    "preview": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Data;\r\nusing System.Linq;\r\nusing System.Net.Http;\r\nusing "
  },
  {
    "path": "BBDown/BBDownAria2c.cs",
    "chars": 1263,
    "preview": "using System.Diagnostics;\nusing System.IO;\nusing System.Threading.Tasks;\n\nnamespace BBDown;\n\nstatic class BBDownAria2c\n"
  },
  {
    "path": "BBDown/BBDownConfigParser.cs",
    "chars": 2287,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.CommandLine.Parsing;\nusing System.CommandLine;\nusing Syste"
  },
  {
    "path": "BBDown/BBDownDownloadUtil.cs",
    "chars": 9054,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net.Http;\nusing System"
  },
  {
    "path": "BBDown/BBDownEnums.cs",
    "chars": 822,
    "preview": "using System;\r\nusing System.Linq;\r\n\r\nnamespace BBDown;\r\n\r\npublic enum BBDownDanmakuFormat\r\n{\r\n    Xml,\r\n    Ass,\r\n}\r\n\r\np"
  },
  {
    "path": "BBDown/BBDownLoginUtil.cs",
    "chars": 5657,
    "preview": "using QRCoder;\nusing System;\nusing System.IO;\nusing System.Threading.Tasks;\nusing static BBDown.BBDownUtil;\nusing stati"
  },
  {
    "path": "BBDown/BBDownMuxer.cs",
    "chars": 9673,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Text;\nusing s"
  },
  {
    "path": "BBDown/BBDownUtil.cs",
    "chars": 22591,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Collections.Specialized;\nusing System.Diagnostics;\nusing Sy"
  },
  {
    "path": "BBDown/CommandLineInvoker.cs",
    "chars": 17858,
    "preview": "using System;\nusing System.CommandLine;\nusing System.CommandLine.Binding;\nusing System.CommandLine.Parsing;\nusing Syste"
  },
  {
    "path": "BBDown/ConsoleQRCode.cs",
    "chars": 1038,
    "preview": "using QRCoder;\nusing System;\n\nnamespace BBDown;\n\npublic class ConsoleQRCode : AbstractQRCode\n{\n    public ConsoleQRCode"
  },
  {
    "path": "BBDown/Directory.Build.props",
    "chars": 543,
    "preview": "<Project>\n\n  <PropertyGroup>\n    <IlcOptimizationPreference>Speed</IlcOptimizationPreference>\n    <IlcFoldIdenticalMetho"
  },
  {
    "path": "BBDown/Model/ServeRequestOptions.cs",
    "chars": 175,
    "preview": "using BBDown;\n\ninternal class ServeRequestOptions : MyOption\n{\n\n    /// <summary>\n    /// 任务完成回调Http请求地址\n    /// </summa"
  },
  {
    "path": "BBDown/MyOption.cs",
    "chars": 2771,
    "preview": "namespace BBDown;\n\ninternal class MyOption\n{\n    public string Url { get; set; } = default!;\n    public bool UseTvApi {"
  },
  {
    "path": "BBDown/Program.Methods.cs",
    "chars": 20225,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Threading.Tasks;\nusing static BBDown.Core"
  },
  {
    "path": "BBDown/Program.cs",
    "chars": 39223,
    "preview": "using System;\r\nusing System.Collections.Generic;\r\nusing System.CommandLine;\r\nusing System.CommandLine.Parsing;\r\nusing Sy"
  },
  {
    "path": "BBDown/ProgressBar.cs",
    "chars": 4359,
    "preview": "using System;\nusing System.Text;\nusing System.Threading;\n\n/**\n * From https://gist.github.com/DanielSWolf/0ab6a96899cc5"
  },
  {
    "path": "BBDown/Properties/launchSettings.json",
    "chars": 467,
    "preview": "{\r\n  \"profiles\": {\r\n    \"BBDown\": {\r\n      \"commandName\": \"Project\",\r\n      \"environmentVariables\": {\r\n        \"ASPNETCO"
  },
  {
    "path": "BBDown.Core/APP/Header/device.proto",
    "chars": 544,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage Device {\n    optional int32 appId = 1;\n   "
  },
  {
    "path": "BBDown.Core/APP/Header/fawkesreq.proto",
    "chars": 187,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage FawkesReq {\n    optional string appkey = 1"
  },
  {
    "path": "BBDown.Core/APP/Header/locale.proto",
    "chars": 300,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage Locale {\n    message LocaleIds {\n        o"
  },
  {
    "path": "BBDown.Core/APP/Header/metadata.proto",
    "chars": 318,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage Metadata {\n    optional string accessKey ="
  },
  {
    "path": "BBDown.Core/APP/Header/network.proto",
    "chars": 482,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage Network {\n    enum TYPE {\n        NT_UNKNO"
  },
  {
    "path": "BBDown.Core/APP/Header/restriction.proto",
    "chars": 309,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage Restriction {\n    optional bool teenagersM"
  },
  {
    "path": "BBDown.Core/APP/Payload/dmviewreq.proto",
    "chars": 242,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage DmViewReq {\n    optional int64 pid = 1;\n  "
  },
  {
    "path": "BBDown.Core/APP/Payload/playviewreq.proto",
    "chars": 659,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage PlayViewReq {\n    optional int64 epId = 1;"
  },
  {
    "path": "BBDown.Core/APP/Response/dmviewreply.proto",
    "chars": 3349,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage DmViewReply {\n    optional bool closed = 1"
  },
  {
    "path": "BBDown.Core/APP/Response/playviewreply.proto",
    "chars": 4011,
    "preview": "syntax = \"proto2\";\noption csharp_namespace = \"BBDown.Core.Protobuf\";\n\nmessage VideoInfo {\n    optional uint32 quality = "
  },
  {
    "path": "BBDown.Core/AppHelper.cs",
    "chars": 19895,
    "preview": "using BBDown.Core.Protobuf;\nusing Google.Protobuf;\nusing System.Buffers.Binary;\nusing System.IO.Compression;\nusing Syst"
  },
  {
    "path": "BBDown.Core/BBDown.Core.csproj",
    "chars": 664,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>library</OutputType>\n    <TargetFramework>net9.0</"
  },
  {
    "path": "BBDown.Core/Config.cs",
    "chars": 1046,
    "preview": "namespace BBDown.Core;\n\npublic static class Config\n{\n    //For WEB\n    public static string COOKIE { get; set; } = \"\";\n"
  },
  {
    "path": "BBDown.Core/DanmakuUtil.cs",
    "chars": 8659,
    "preview": "using static BBDown.Core.Logger;\nusing System.Text;\nusing System.Xml;\n\nnamespace BBDown.Core;\n\npublic static class Danm"
  },
  {
    "path": "BBDown.Core/Entity/Entity.cs",
    "chars": 6539,
    "preview": "using BBDown.Core.Util;\nusing System.Diagnostics.CodeAnalysis;\n\nnamespace BBDown.Core.Entity;\n\npublic static class Enti"
  },
  {
    "path": "BBDown.Core/Entity/ParsedResult.cs",
    "chars": 598,
    "preview": "using static BBDown.Core.Entity.Entity;\n\nnamespace BBDown.Core.Entity;\n\npublic class ParsedResult\n{\n    public string W"
  },
  {
    "path": "BBDown.Core/Entity/VInfo.cs",
    "chars": 976,
    "preview": "using static BBDown.Core.Entity.Entity;\n\nnamespace BBDown.Core.Entity;\n\npublic class VInfo\n{\n    /// <summary>\n    /// "
  },
  {
    "path": "BBDown.Core/Fetcher/BangumiInfoFetcher.cs",
    "chars": 3101,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing static BBDown.Core.Entity.Entity;\nusing static BBDown.Core.Util"
  },
  {
    "path": "BBDown.Core/Fetcher/CheeseInfoFetcher.cs",
    "chars": 2071,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing static BBDown.Core.Entity.Entity;\nusing static BBDown.Core.Util"
  },
  {
    "path": "BBDown.Core/Fetcher/FavListFetcher.cs",
    "chars": 4212,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing static BBDown.Core.Entity.Entity;\nusing static BBDown.Core.Util"
  },
  {
    "path": "BBDown.Core/Fetcher/IntlBangumiInfoFetcher.cs",
    "chars": 5128,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing static BBDown.Core.Entity"
  },
  {
    "path": "BBDown.Core/Fetcher/MediaListFetcher.cs",
    "chars": 4892,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing static BBDown.Core.Entity.Entity;\nusing static BBDown.Core.Util"
  },
  {
    "path": "BBDown.Core/Fetcher/NormalInfoFetcher.cs",
    "chars": 5032,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing System.Text.RegularExpressions;\nusing System.Xml;\nusing static "
  },
  {
    "path": "BBDown.Core/Fetcher/SeriesListFetcher.cs",
    "chars": 3468,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing static BBDown.Core.Entity.Entity;\nusing static BBDown.Core.Util"
  },
  {
    "path": "BBDown.Core/Fetcher/SpaceVideoFetcher.cs",
    "chars": 3275,
    "preview": "using BBDown.Core.Entity;\nusing System.Text.Json;\nusing static BBDown.Core.Util.HTTPUtil;\nusing static BBDown.Core.Logg"
  },
  {
    "path": "BBDown.Core/FetcherFactory.cs",
    "chars": 1104,
    "preview": "using BBDown.Core.Fetcher;\n\nnamespace BBDown.Core;\n\npublic static class FetcherFactory\n{\n    /// <summary>\n    /// 根据不同"
  },
  {
    "path": "BBDown.Core/IFetcher.cs",
    "chars": 100,
    "preview": "namespace BBDown.Core;\n\npublic interface IFetcher\n{\n    Task<Entity.VInfo> FetchAsync(string id);\n}"
  },
  {
    "path": "BBDown.Core/Logger.cs",
    "chars": 1897,
    "preview": "namespace BBDown.Core;\n\npublic static class Logger\n{\n    public static void Log(object text, bool enter = true)\n    {\n "
  },
  {
    "path": "BBDown.Core/Parser.cs",
    "chars": 21326,
    "preview": "using System.Text;\nusing System.Text.RegularExpressions;\nusing System.Text.Json;\nusing static BBDown.Core.Logger;\nusing"
  },
  {
    "path": "BBDown.Core/Util/BilibiliBvConverter.cs",
    "chars": 2011,
    "preview": "using System.Text;\n\nnamespace BBDown.Core.Util;\n\n//code from: https://github.com/Colerar/abv/blob/main/src/lib.rs\npubli"
  },
  {
    "path": "BBDown.Core/Util/HTTPUtil.cs",
    "chars": 4812,
    "preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing static BBDown.Core.Logger;\n\nnamespace BBDown.Core.Util;\n\npublic "
  },
  {
    "path": "BBDown.Core/Util/SubUtil.cs",
    "chars": 20964,
    "preview": "using BBDown.Core.Protobuf;\nusing Google.Protobuf;\nusing System.Text;\nusing static BBDown.Core.Entity.Entity;\nusing sta"
  },
  {
    "path": "BBDown.sln",
    "chars": 1578,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.2.3221"
  },
  {
    "path": "Dockerfile",
    "chars": 394,
    "preview": "FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder\n\nWORKDIR /app\n\nCOPY . .\n\nRUN dotnet build -c Release\n\nFROM mcr.microsof"
  },
  {
    "path": "LICENSE",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2020 nilaoda\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "README.md",
    "chars": 10728,
    "preview": "[![img](https://img.shields.io/github/stars/nilaoda/BBDown?label=%E7%82%B9%E8%B5%9E)](https://github.com/nilaoda/BBDown)"
  },
  {
    "path": "json-api-doc.md",
    "chars": 3312,
    "preview": "# JSON API文档\n\n## API\n\n如果以服务器模式启动BBDown,BBDown会在本地启动一个http server,该服务器有以下API:\n\n### 获取任务列表\n**Endpoint:** `/get-tasks/`\n\n**"
  }
]

About this extraction

This page contains the full source code of the nilaoda/BBDown GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 60 files (308.8 KB), approximately 79.7k tokens, and a symbol index with 281 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!