Full Code of nilaoda/N_m3u8DL-RE for AI

main a441ea83f8e7 cached
114 files
614.5 KB
160.4k tokens
522 symbols
1 requests
Download .txt
Showing preview only (651K chars total). Download the full file or copy to clipboard to get everything.
Repository: nilaoda/N_m3u8DL-RE
Branch: main
Commit: a441ea83f8e7
Files: 114
Total size: 614.5 KB

Directory structure:
gitextract_dnlao_8d/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── build_latest.yml
├── .gitignore
├── LICENSE
├── README.en.md
├── README.md
├── TestStreams.md
└── src/
    ├── N_m3u8DL-RE/
    │   ├── Column/
    │   │   ├── DownloadSpeedColumn.cs
    │   │   ├── DownloadStatusColumn.cs
    │   │   ├── MyPercentageColumn.cs
    │   │   ├── RecordingDurationColumn.cs
    │   │   ├── RecordingSizeColumn.cs
    │   │   └── RecordingStatusColumn.cs
    │   ├── CommandLine/
    │   │   ├── CommandInvoker.cs
    │   │   ├── ComplexParamParser.cs
    │   │   └── MyOption.cs
    │   ├── Config/
    │   │   ├── DownloaderConfig.cs
    │   │   └── EnvConfigKey.cs
    │   ├── Crypto/
    │   │   ├── AESUtil.cs
    │   │   ├── CSChaCha20.cs
    │   │   └── ChaCha20Util.cs
    │   ├── Directory.Build.props
    │   ├── DownloadManager/
    │   │   ├── HTTPLiveRecordManager.cs
    │   │   ├── SimpleDownloadManager.cs
    │   │   └── SimpleLiveRecordManager2.cs
    │   ├── Downloader/
    │   │   ├── IDownloader.cs
    │   │   └── SimpleDownloader.cs
    │   ├── Entity/
    │   │   ├── CustomRange.cs
    │   │   ├── DownloadResult.cs
    │   │   ├── Mediainfo.cs
    │   │   ├── MuxOptions.cs
    │   │   ├── OutputFile.cs
    │   │   ├── SpeedContainer.cs
    │   │   └── StreamFilter.cs
    │   ├── Enum/
    │   │   ├── DecryptEngine.cs
    │   │   ├── MuxFormat.cs
    │   │   └── SubtitleFormat.cs
    │   ├── N_m3u8DL-RE.csproj
    │   ├── Processor/
    │   │   ├── DemoProcessor.cs
    │   │   ├── DemoProcessor2.cs
    │   │   └── NowehoryzontyUrlProcessor.cs
    │   ├── Program.cs
    │   └── Util/
    │       ├── CultureUtil.cs
    │       ├── DownloadUtil.cs
    │       ├── FilterUtil.cs
    │       ├── ImageHeaderUtil.cs
    │       ├── LanguageCodeUtil.cs
    │       ├── LargeSingleFileSplitUtil.cs
    │       ├── MP4DecryptUtil.cs
    │       ├── MediainfoUtil.cs
    │       ├── MergeUtil.cs
    │       ├── OtherUtil.cs
    │       ├── PipeUtil.cs
    │       └── SubtitleUtil.cs
    ├── N_m3u8DL-RE.Common/
    │   ├── Entity/
    │   │   ├── EncryptInfo.cs
    │   │   ├── MSSData.cs
    │   │   ├── MediaPart.cs
    │   │   ├── MediaSegment.cs
    │   │   ├── Playlist.cs
    │   │   ├── StreamSpec.cs
    │   │   ├── SubCue.cs
    │   │   └── WebVttSub.cs
    │   ├── Enum/
    │   │   ├── Choise.cs
    │   │   ├── EncryptMethod.cs
    │   │   ├── ExtractorType.cs
    │   │   ├── MediaType.cs
    │   │   └── RoleType.cs
    │   ├── JsonContext/
    │   │   └── JsonContext.cs
    │   ├── JsonConverter/
    │   │   └── BytesBase64Converter.cs
    │   ├── Log/
    │   │   ├── CustomAnsiConsole.cs
    │   │   ├── LogLevel.cs
    │   │   └── Logger.cs
    │   ├── N_m3u8DL-RE.Common.csproj
    │   ├── Resource/
    │   │   ├── ResString.cs
    │   │   ├── StaticText.cs
    │   │   └── TextContainer.cs
    │   └── Util/
    │       ├── BinaryContentCheckUtil.cs
    │       ├── GlobalUtil.cs
    │       ├── HTTPUtil.cs
    │       ├── HexUtil.cs
    │       └── RetryUtil.cs
    ├── N_m3u8DL-RE.Parser/
    │   ├── Config/
    │   │   └── ParserConfig.cs
    │   ├── Constants/
    │   │   ├── DASHTags.cs
    │   │   ├── HLSTags.cs
    │   │   └── MSSTags.cs
    │   ├── Extractor/
    │   │   ├── DASHExtractor2.cs
    │   │   ├── HLSExtractor.cs
    │   │   ├── IExtractor.cs
    │   │   ├── LiveTSExtractor.cs
    │   │   └── MSSExtractor.cs
    │   ├── InternalsVisibleTo.cs
    │   ├── Mp4/
    │   │   ├── BinaryReader2.cs
    │   │   ├── BinaryWriter2.cs
    │   │   ├── MP4InitUtil.cs
    │   │   ├── MP4Parser.cs
    │   │   ├── MP4TtmlUtil.cs
    │   │   ├── MP4VttUtil.cs
    │   │   └── MSSMoovProcessor.cs
    │   ├── N_m3u8DL-RE.Parser.csproj
    │   ├── Processor/
    │   │   ├── ContentProcessor.cs
    │   │   ├── DASH/
    │   │   │   └── DefaultDASHContentProcessor.cs
    │   │   ├── DefaultUrlProcessor.cs
    │   │   ├── HLS/
    │   │   │   ├── DefaultHLSContentProcessor.cs
    │   │   │   └── DefaultHLSKeyProcessor.cs
    │   │   ├── KeyProcessor.cs
    │   │   └── UrlProcessor.cs
    │   ├── StreamExtractor.cs
    │   └── Util/
    │       └── ParserUtil.cs
    ├── N_m3u8DL-RE.Tests/
    │   ├── Common/
    │   │   └── Util/
    │   │       └── HexUtilTests.cs
    │   ├── N_m3u8DL-RE.Tests.csproj
    │   ├── Parser/
    │   │   └── Extractor/
    │   │       └── DASHExtractor2Tests.cs
    │   ├── ResourceHelper.cs
    │   └── Resources/
    │       └── Dash/
    │           └── Manifest_1080p.mpd
    └── N_m3u8DL-RE.sln

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://nilaoda.github.io/N_m3u8DL-CLI/source/images/alipay.png','https://www.buymeacoffee.com/nilaoda']


================================================
FILE: .github/workflows/build_latest.yml
================================================
name: Build Latest

on:
  workflow_dispatch:
    inputs:
      doRelease:
        description: 'Publish new release'
        type: boolean
        default: false
        required: false
      tag:
        type: string
        description: 'Release version tag (e.g. v0.2.1-beta)'
        required: true
      ref:
        type: string
        description: 'Git ref from which to release'
        required: true
        default: 'main'

env:
  DOTNET_SDK_VERSION: "10.0.101"
  ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true

jobs:
  set-date:
    runs-on: ubuntu-latest
    outputs:
      date: ${{ steps.get_date.outputs.date }}
      tag: ${{ steps.format_tag.outputs.tag }}
    steps:
      - name: Get Date in UTC+8
        id: get_date
        run: |
          DATE=$(date -u -d '8 hours' +'%Y%m%d')
          echo "date=${DATE}" >> "$GITHUB_OUTPUT"

      - name: Determine Tag
        id: format_tag
        run: |
          if [ "${{ github.event.inputs.doRelease }}" == "true" ]; then
            TAG="${{ github.event.inputs.tag }}"
          else
            TAG="actions-$GITHUB_RUN_ID"
          fi
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"

  build-win-nt6_0-x86:
    runs-on: windows-latest
    needs: set-date

    steps:
      - uses: actions/checkout@v1
        with:
          ref: ${{ github.event.inputs.ref }}

      - name: Install zip
        run: choco install zip --no-progress --yes

      - name: Set up dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}

      - run: powershell -Command "(Get-Content src/N_m3u8DL-RE/N_m3u8DL-RE.csproj) -replace '<TargetFramework>.*</TargetFramework>', '<TargetFramework>net10.0-windows</TargetFramework>' | Set-Content src/N_m3u8DL-RE/N_m3u8DL-RE.csproj"
      - run: dotnet add src/N_m3u8DL-RE/N_m3u8DL-RE.csproj package YY-Thunks --version 1.1.4
      - run: dotnet add src/N_m3u8DL-RE/N_m3u8DL-RE.csproj package VC-LTL --version 5.1.1
      - run: dotnet publish src/N_m3u8DL-RE -p:TargetPlatformMinVersion=6.0 -r win-x86 -c Release -o artifact-x86

      - name: Package [win-x86]
        run: |
          cd artifact-x86
          zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-NT6.0-x86_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe

      - name: Upload Artifact[win-x86]
        uses: actions/upload-artifact@v4
        with:
          name: win-NT6.0-x86
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-NT6.0-x86_${{ needs.set-date.outputs.date }}.zip

  build-win-x64-arm64:
    runs-on: windows-latest
    needs: set-date

    steps:
      - uses: actions/checkout@v1
        with:
          ref: ${{ github.event.inputs.ref }}

      - name: Install zip
        run: choco install zip --no-progress --yes

      - name: Set up dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}

      - run: dotnet publish src/N_m3u8DL-RE -r win-x64 -c Release -o artifact-x64
      - run: dotnet publish src/N_m3u8DL-RE -r win-arm64 -c Release -o artifact-arm64

      - name: Package [win]
        run: |
          cd artifact-x64
          zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-x64_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe
          cd ../artifact-arm64
          zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-arm64_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe

      - name: Upload Artifact [win-x64]
        uses: actions/upload-artifact@v4
        with:
          name: win-x64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-x64_${{ needs.set-date.outputs.date }}.zip

      - name: Upload Artifact [win-arm64]
        uses: actions/upload-artifact@v4
        with:
          name: win-arm64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-arm64_${{ needs.set-date.outputs.date }}.zip

  build-android-bionic-x64-arm64:
    runs-on: windows-latest
    needs: set-date

    steps:
      - uses: actions/checkout@v1
        with:
          ref: ${{ github.event.inputs.ref }}

      - name: Set up NDK
        shell: pwsh
        run: |
          Invoke-WebRequest -Uri "https://dl.google.com/android/repository/android-ndk-r27c-windows.zip" -OutFile "android-ndk.zip"
          Expand-Archive -Path "android-ndk.zip" -DestinationPath "./android-ndk"
          Get-ChildItem -Path "./android-ndk"
          $ndkRoot = "${{ github.workspace }}\android-ndk\android-ndk-r27c"
          echo "NDK_ROOT=$ndkRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8
          $ndkBinPath = "$ndkRoot\toolchains\llvm\prebuilt\windows-x86_64\bin"
          echo $ndkBinPath | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8

      - name: Set up dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}

      - run: dotnet publish src/N_m3u8DL-RE -r linux-bionic-x64 -p:DisableUnsupportedError=true -p:PublishAotUsingRuntimePack=true -o artifact
      - run: dotnet publish src/N_m3u8DL-RE -r linux-bionic-arm64 -p:DisableUnsupportedError=true -p:PublishAotUsingRuntimePack=true -o artifact-arm64

      - name: Package [linux-bionic]
        run: |
          cd artifact
          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
          cd ../artifact-arm64
          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE

      - name: Upload Artifact [linux-bionic-x64]
        uses: actions/upload-artifact@v4
        with:
          name: android-bionic-x64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-x64_${{ needs.set-date.outputs.date }}.tar.gz

      - name: Upload Artifact[linux-bionic-arm64]
        uses: actions/upload-artifact@v4
        with:
          name: android-bionic-arm64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-arm64_${{ needs.set-date.outputs.date }}.tar.gz

  build-linux-x64:
    runs-on: ubuntu-latest
    needs: set-date

    steps:
      - uses: actions/checkout@v1
        with:
          ref: ${{ github.event.inputs.ref }}

      - name: Build x64 in alpine container
        run: |
          # 在 alpine 容器中执行完整构建流程
          docker run --rm \
            -v "$PWD:/workspace" \
            -w /workspace \
            alpine:3.21 \
            sh -c "
              set -e
          
              # 安装编译和运行依赖
              apk add --no-cache bash wget tar clang build-base cmake icu-dev icu-data-full zlib-static openssl-dev openssl-libs-static
  
              # 下载并安装 .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-musl-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 src/N_m3u8DL-RE -r linux-musl-x64 -c Release -o /workspace/artifact
            "

      - name: Package [linux-x64]
        run: |
          cd artifact
          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE

      - name: Upload Artifact [linux-x64]
        uses: actions/upload-artifact@v4
        with:
          name: linux-x64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-x64_${{ needs.set-date.outputs.date }}.tar.gz

  build-linux-arm64:
    runs-on: ubuntu-latest
    needs: set-date

    steps:
      - uses: actions/checkout@v1
        with:
          ref: ${{ github.event.inputs.ref }}

      - name: Build arm64 in alpine container
        run: |
          # 在交叉编译环境容器中执行完整构建流程
          docker run --rm \
            -v "$PWD:/workspace" \
            -w /workspace \
            mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-cross-arm64-alpine \
            bash -c "
              set -e
              
              # 确保 Ubuntu 宿主环境有所有依赖
              apt-get update && apt-get install -y build-essential clang binutils-aarch64-linux-gnu
          
              # 动态获取 Alpine 最新 OpenSSL 版本号
              ALPINE_REPO='https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64'
              echo \"Fetching latest OpenSSL version from Alpine...\"
              # 自动抓取 openssl-dev 的完整文件名并解析版本号
              SSL_APK_NAME=\$(wget -qO- \$ALPINE_REPO/ | grep -o 'openssl-dev-[0-9.]\+-r[0-9]\+.apk' | head -n 1)
              SSL_VER=\$(echo \$SSL_APK_NAME | sed 's/openssl-dev-//;s/.apk//')
              echo \"Detected OpenSSL Version: \$SSL_VER\"
              
              # 下载并提取 ARM64 的 OpenSSL 静态库到 SysRoot
              mkdir -p /tmp/ssl_setup && cd /tmp/ssl_setup
              wget -q \$ALPINE_REPO/openssl-libs-static-\$SSL_VER.apk
              wget -q \$ALPINE_REPO/openssl-dev-\$SSL_VER.apk
              
              # 解压到交叉编译系统的根目录
              for f in *.apk; do tar -xf \$f -C /crossrootfs/arm64; done
              cd /workspace
              
              # 设置交叉编译环境变量 (强制 CMake 和编译器使用正确路径)
              export CROSS_COMPILE_PREFIX=\"aarch64-alpine-linux-musl-\"
              export CC=\"\${CROSS_COMPILE_PREFIX}clang\"
              export CXX=\"\${CROSS_COMPILE_PREFIX}clang++\"
              
              # 告知 .NET 本地脚本去哪里找 OpenSSL 静态库
              export OPENSSL_ROOT_DIR=\"/crossrootfs/arm64/usr\"
              export OPENSSL_USE_STATIC_LIBS=TRUE
              
              # 额外保险:设置 CFLAGS 让 CMake 内部探测更准确
              export CFLAGS=\"--sysroot=/crossrootfs/arm64 --target=aarch64-alpine-linux-musl\"
          
              # 下载并安装 .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\"
          
              # 交叉编译
              dotnet publish src/N_m3u8DL-RE -r linux-musl-arm64 -c Release \
                -p:SysRoot=/crossrootfs/arm64 \
                -p:CppCompilerAndLinker=clang \
                -p:StripSymbols=true \
                -o /workspace/artifact
            "

      - name: Package [linux-arm64]
        run: |
          cd artifact
          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE

      - name: Upload Artifact [linux-x64]
        uses: actions/upload-artifact@v4
        with:
          name: linux-arm64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-arm64_${{ needs.set-date.outputs.date }}.tar.gz

  build-mac-x64-arm64:
    runs-on: macos-latest
    needs: set-date

    steps:
      - uses: actions/checkout@v1
        with:
          ref: ${{ github.event.inputs.ref }}

      - name: Set up dotnet
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
      - run: dotnet publish src/N_m3u8DL-RE -r osx-arm64 -c Release -o artifact-arm64 
      - run: dotnet publish src/N_m3u8DL-RE -r osx-x64 -c Release -o artifact-x64 

      - name: Package [osx]
        run: |
          cd artifact-x64
          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
          cd ../artifact-arm64
          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE

      - name: Upload Artifact [osx-x64]
        uses: actions/upload-artifact@v4
        with:
          name: osx-x64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-x64_${{ needs.set-date.outputs.date }}.tar.gz

      - name: Upload Artifact[osx-arm64]
        uses: actions/upload-artifact@v4
        with:
          name: osx-arm64
          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-arm64_${{ needs.set-date.outputs.date }}.tar.gz

  create_release:
    name: Create release
    runs-on: ubuntu-latest
    permissions:
      contents: write
    if: ${{ github.event.inputs.doRelease == 'true' }}
    needs: [set-date,build-win-nt6_0-x86,build-win-x64-arm64,build-android-bionic-x64-arm64,build-linux-x64,build-linux-arm64,build-mac-x64-arm64]

    steps:
      - name: Fetch artifacts
        uses: actions/download-artifact@v4

      - name: Create GitHub Release
        uses: ncipollo/release-action@v1
        with:
          tag: ${{ github.event.inputs.tag }}
          name: N_m3u8DL-RE_${{ github.event.inputs.tag }}
          artifacts: "android-bionic-x64/*,android-bionic-arm64/*,linux-x64/*,linux-arm64/*,linux-musl-x64/*,linux-musl-arm64/*,osx-x64/*,osx-arm64/*,win-x64/*,win-arm64/*,win-NT6.0-x86/*"
          draft: false
          allowUpdates: true
          generateReleaseNotes: true
          discussionCategory: 'Announcements'

================================================
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.*

# Properties
Properties/

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

# Rider
.idea

# 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

# macOS shit
.DS_Store


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022 nilaoda

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.en.md
================================================
# N_m3u8DL-RE [EN]

Cross-platform DASH/HLS/MSS download tool. Supports on-demand and live streaming (DASH/HLS).

[![img](https://img.shields.io/github/stars/nilaoda/N_m3u8DL-RE?label=%E7%82%B9%E8%B5%9E)](https://github.com/nilaoda/N_m3u8DL-RE)  [![img](https://img.shields.io/github/last-commit/nilaoda/N_m3u8DL-RE?label=%E6%9C%80%E8%BF%91%E6%8F%90%E4%BA%A4)](https://github.com/nilaoda/N_m3u8DL-RE)  [![img](https://img.shields.io/github/release/nilaoda/N_m3u8DL-RE?label=%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)](https://github.com/nilaoda/N_m3u8DL-RE/releases)  [![img](https://img.shields.io/github/license/nilaoda/N_m3u8DL-RE?label=%E8%AE%B8%E5%8F%AF%E8%AF%81)](https://github.com/nilaoda/N_m3u8DL-RE)   [![img](https://img.shields.io/github/downloads/nilaoda/N_m3u8DL-RE/total?label=%E4%B8%8B%E8%BD%BD%E9%87%8F)](https://github.com/nilaoda/N_m3u8DL-RE/releases)

If you encounter a bug, please first confirm whether you are using the latest version of the software. (If you are using a release version, it is recommended to go to the [Actions](https://github.com/nilaoda/N_m3u8DL-RE/actions) page to download the latest automatically built version and check if the issue has already been fixed.) If you are using the latest version and the issue still exists, you can check the [Issues](https://github.com/nilaoda/N_m3u8DL-RE/issues) section to see if someone else has encountered a similar problem. If not, feel free to open a new issue.

---

The built-in terminal in older versions of Windows may not support this program. As an alternative, try running it in [cmder](https://github.com/cmderdev/cmder).

Arch Linux users can install from AUR: [n-m3u8dl-re-bin](https://aur.archlinux.org/packages/n-m3u8dl-re-bin), [n-m3u8dl-re-git](https://aur.archlinux.org/packages/n-m3u8dl-re-git)

```bash
# Install N_m3u8DL-RE release version on Arch Linux and its derivatives (not maintained by the author)
yay -Syu n-m3u8dl-re-bin

# Install N_m3u8DL-RE development version on Arch Linux and its derivatives (not maintained by the author)
yay -Syu n-m3u8dl-re-git
```

---

## Command line parameters

```
Description:
  N_m3u8DL-RE (Beta version) 20241203

Usage:
  N_m3u8DL-RE <input> [options]

Arguments:
  <input>  Input Url or File

Options:
  --tmp-dir <tmp-dir>                                     Set temporary file directory
  --save-dir <save-dir>                                   Set output directory
  --save-name <save-name>                                 Set output filename
  --base-url <base-url>                                   Set BaseURL
  --thread-count <number>                                 Set download thread count [default: based on the number of CPU cores]
  --download-retry-count <number>                         The number of retries when download segment error [default: 3]
  --http-request-timeout <seconds>                        Timeout duration for HTTP requests (in seconds) [default: 100]
  --force-ansi-console                                    Force assuming the terminal is ANSI-compatible and interactive
  --no-ansi-color                                         Remove ANSI colors
  --auto-select                                           Automatically selects the best tracks of all types [default:
                                                          False]
  --skip-merge                                            Skip segments merge [default: False]
  --skip-download                                         Skip download [default: False]
  --check-segments-count                                  Check if the actual number of segments downloaded matches the
                                                          expected number [default: True]
  --binary-merge                                          Binary merge [default: False]
  --use-ffmpeg-concat-demuxer                             When merging with ffmpeg, use the concat demuxer instead of
                                                          the concat protocol [default: False]
  --del-after-done                                        Delete temporary files when done [default: True]
  --no-date-info                                          Date information is not written during muxing [default: False]
  --no-log                                                Disable log file output [default: False]
  --write-meta-json                                       Write meta json after parsed [default: True]
  --append-url-params                                     Add Params of input Url to segments, useful for some
                                                          websites, such as kakao.com [default: False]
  -mt, --concurrent-download                              Concurrently download the selected audio, video and subtitles
                                                          [default: False]
  -H, --header <header>                                   Pass custom header(s) to server, Example:
                                                          -H "Cookie: mycookie" -H "User-Agent: iOS"
  --sub-only                                              Select only subtitle tracks [default: False]
  --sub-format <SRT|VTT>                                  Subtitle output format [default: SRT]
  --auto-subtitle-fix                                     Automatically fix subtitles [default: True]
  --ffmpeg-binary-path <PATH>                             Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe
  --log-level <DEBUG|ERROR|INFO|OFF|WARN>                 Set log level [default: INFO]
  --ui-language <en-US|zh-CN|zh-TW>                       Set UI language
  --urlprocessor-args <urlprocessor-args>                 Give these arguments to the URL Processors.
  --key <key>                                             Set decryption key(s) to mp4decrypt/shaka-packager/ffmpeg.
                                                          format:
                                                          --key KID1:KEY1 --key KID2:KEY2
                                                          or use --key KEY if all tracks share the same key.
  --key-text-file <key-text-file>                         Set the kid-key file, the program will search the KEY with
                                                          KID from the file.(Very large file are not recommended)
  --decryption-engine <FFMPEG|MP4DECRYPT|SHAKA_PACKAGER>  Set the third-party program used for decryption [default:
                                                          MP4DECRYPT]
  --decryption-binary-path <PATH>                         Full path to the tool used for MP4 decryption, like
                                                          C:\Tools\mp4decrypt.exe
  --mp4-real-time-decryption                              Decrypt MP4 segments in real time [default: False]
  -R, --max-speed <SPEED>                                 Set speed limit, Mbps or Kbps, for example: 15M 100K.
  -M, --mux-after-done <OPTIONS>                          When all works is done, try to mux the downloaded streams.
                                                          Use "--morehelp mux-after-done" for more details
  --custom-hls-method <METHOD>                            Set HLS encryption method
                                                          (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_
                                                          CTR|UNKNOWN)
  --custom-hls-key <FILE|HEX|BASE64>                      Set the HLS decryption key. Can be file, HEX or Base64
  --custom-hls-iv <FILE|HEX|BASE64>                       Set the HLS decryption iv. Can be file, HEX or Base64
  --use-system-proxy                                      Use system default proxy [default: True]
  --custom-proxy <URL>                                    Set web request proxy, like http://127.0.0.1:8888
  --custom-range <RANGE>                                  Download only part of the segments. Use "--morehelp
                                                          custom-range" for more details
  --task-start-at <yyyyMMddHHmmss>                        Task execution will not start before this time
  --live-perform-as-vod                                   Download live streams as vod [default: False]
  --live-real-time-merge                                  Real-time merge into file when recording live [default: False]
  --live-keep-segments                                    Keep segments when recording a live (liveRealTimeMerge
                                                          enabled) [default: True]
  --live-pipe-mux                                         Real-time muxing to TS file through pipeline + ffmpeg
                                                          (liveRealTimeMerge enabled) [default: False]
  --live-fix-vtt-by-audio                                 Correct VTT sub by reading the start time of the audio file
                                                          [default: False]
  --live-record-limit <HH:mm:ss>                          Recording time limit when recording live
  --live-wait-time <SEC>                                  Manually set the live playlist refresh interval
  --live-take-count <NUM>                                 Manually set the number of segments downloaded for the first
                                                          time when recording live [default: 16]
  --mux-import <OPTIONS>                                  When MuxAfterDone enabled, allow to import local media files.
                                                          Use "--morehelp mux-import" for more details
  -sv, --select-video <OPTIONS>                           Select video streams by regular expressions. Use "--morehelp
                                                          select-video" for more details
  -sa, --select-audio <OPTIONS>                           Select audio streams by regular expressions. Use "--morehelp
                                                          select-audio" for more details
  -ss, --select-subtitle <OPTIONS>                        Select subtitle streams by regular expressions. Use
                                                          "--morehelp select-subtitle" for more details
  -dv, --drop-video <OPTIONS>                             Drop video streams by regular expressions.
  -da, --drop-audio <OPTIONS>                             Drop audio streams by regular expressions.
  -ds, --drop-subtitle <OPTIONS>                          Drop subtitle streams by regular expressions.
  --ad-keyword <REG>                                      Set URL keywords (regular expressions) for AD segments
  --disable-update-check                                  Disable version update check [default: False]
  --allow-hls-multi-ext-map                               Allow multiple #EXT-X-MAP in HLS (experimental) [default:
                                                          False]
  --morehelp <OPTION>                                     Set more help info about one option
  --version                                               Show version information
  -?, -h, --help                                          Show help and usage information
```

<details>
<summary>Click to view "More Help" section</summary>

```
More Help:

  --mux-after-done

所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:

* format=FORMAT: 指定混流容器 mkv, mp4
* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)
* bin_path=PATH: 指定程序路径 (默认: 自动寻找)
* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)
* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)

例如:
# 混流为mp4容器
-M format=mp4
# 使用mkvmerge, 自动寻找程序
-M format=mkv:muxer=mkvmerge
# 使用mkvmerge, 自定义程序路径
-M format=mkv:muxer=mkvmerge:bin_path="C\:\Program Files\MKVToolNix\mkvmerge.exe"
```

```
More Help:

  --mux-import

When MuxAfterDone enabled, allow to import local media files. OPTIONS is a colon separated list of:

* path=PATH: set file path
* lang=CODE: set media language code (not required)
* name=NAME: set description (not required)

Examples:
# import subtitle
--mux-import path=en-US.srt:lang=eng:name="English (Original)"
# import audio and subtitle
--mux-import path="D\:\media\atmos.m4a":lang=eng:name="English Description Audio" --mux-import path="D\:\media\eng.vtt":lang=eng:name="English (Description)"
```

```
More Help:

  --select-video

Select video streams by regular expressions. OPTIONS is a colon separated list of:

id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX
segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX
plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR

* for=FOR: Select type. best[number], worst[number], all (Default: best)

Examples:
# select best video
-sv best
# select 4K+HEVC video
-sv res="3840*":codecs=hvc1:for=best
# Select best video with duration longer than 1 hour 20 minutes 30 seconds
-sv plistDurMin="1h20m30s":for=best
-sv role="main":for=best
# Select video with bandwidth between 800Kbps and 1Mbps
-sv bwMin=800:bwMax=1000
```

```
More Help:

  --select-audio

Select audio streams by regular expressions. ref --select-video

Examples:
# select all
-sa all
# select best eng audio
-sa lang=en:for=best
# select best 2, and language is ja or en
-sa lang="ja|en":for=best2
-sa role="main":for=best
```

```
More Help:

  --select-subtitle

Select subtitle streams by regular expressions. ref --select-video

Examples:
# select all subs
-ss all
# select all subs containing "English"
-ss name="English":for=all
```

```
More Help:

  --custom-range

Download only part of the segments when downloading vod content.

Examples:
# Download [0,10], a total of 11 segments
--custom-range 0-10
# Download subsequent segments starting from index 10
--custom-range 10-
# Download the first 100 segments
--custom-range -99
# Download content from the 05:00 to 20:00
--custom-range 05:00-20:00
```

</details>

## Screenshots

### On-demand

![RE1](img/RE.gif)

Can also download in parallel and automatically mix streams

![RE2](img/RE2.gif)

### Live

Record TS live source:

[click to show gif](http://pan.iqiyi.com/file/paopao/W0LfmaMRvuA--uCdOpZ1cldM5JCVhMfIm7KFqr4oKCz80jLn0bBb-9PWmeCFZ-qHpAaQydQ1zk-CHYT_UbRLtw.gif)

Record MPD live source:

[click to show gif](http://pan.iqiyi.com/file/paopao/nmAV5MOh0yIyHhnxdgM_6th_p2nqrFsM4k-o3cUPwUa8Eh8QOU4uyPkLa_BlBrMa3GBnKWSk8rOaUwbsjKN14g.gif)

During recording, use ffmpeg to mix audio and video in real time

```bash
ffmpeg -readrate 1 -i 2022-09-21_19-54-42_V.mp4 -i 2022-09-21_19-54-42_V.chi.m4a -c copy 2022-09-21_19-54-42_V.ts
```

From v0.1.5, you can try to enable `live-pipe-mux` instead of the above command

> [!NOTE]
> If the network environment is not stable, do not enable `live-pipe-mux`. The data read in the pipeline is handled by ffmpeg, and it is easy to lose live data in some environments.

From v0.1.8, you can set the environment variable `RE_LIVE_PIPE_OPTIONS` to change some options of ffmpeg when `live-pipe-mux` is enabled: <https://github.com/nilaoda/N_m3u8DL-RE/issues/162#issuecomment-1592462532>

## Donate

<a href="https://www.buymeacoffee.com/nilaoda" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>


================================================
FILE: README.md
================================================
# N_m3u8DL-RE

[See English version here](README.en.md)

跨平台的DASH/HLS/MSS下载工具。支持点播、直播(DASH/HLS)。

[![img](https://img.shields.io/github/stars/nilaoda/N_m3u8DL-RE?label=%E7%82%B9%E8%B5%9E)](https://github.com/nilaoda/N_m3u8DL-RE)  [![img](https://img.shields.io/github/last-commit/nilaoda/N_m3u8DL-RE?label=%E6%9C%80%E8%BF%91%E6%8F%90%E4%BA%A4)](https://github.com/nilaoda/N_m3u8DL-RE)  [![img](https://img.shields.io/github/release/nilaoda/N_m3u8DL-RE?label=%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)](https://github.com/nilaoda/N_m3u8DL-RE/releases)  [![img](https://img.shields.io/github/license/nilaoda/N_m3u8DL-RE?label=%E8%AE%B8%E5%8F%AF%E8%AF%81)](https://github.com/nilaoda/N_m3u8DL-RE)   [![img](https://img.shields.io/github/downloads/nilaoda/N_m3u8DL-RE/total?label=%E4%B8%8B%E8%BD%BD%E9%87%8F)](https://github.com/nilaoda/N_m3u8DL-RE/releases)

遇到 BUG 请首先确认软件是否为最新版本(如果是 Release 版本,建议到 [Actions](https://github.com/nilaoda/N_m3u8DL-RE/actions) 页面下载最新自动构建版本后查看问题是否已经被修复),如果确认版本最新且问题依旧存在,可以到 [Issues](https://github.com/nilaoda/N_m3u8DL-RE/issues) 中查找是否有人遇到过相关问题,没有的话再进行询问。

---

版本较低的Windows系统自带的终端可能不支持本程序,替代方案:在 [cmder](https://github.com/cmderdev/cmder) 中运行。

Arch Linux 可以从 AUR 获取:[n-m3u8dl-re-bin](https://aur.archlinux.org/packages/n-m3u8dl-re-bin)、[n-m3u8dl-re-git](https://aur.archlinux.org/packages/n-m3u8dl-re-git)

```bash
# Arch Linux 及其衍生版安装 N_m3u8DL-RE 发行版 (该源非本人维护)
yay -Syu n-m3u8dl-re-bin

# Arch Linux 及其衍生版安装 N_m3u8DL-RE 开发版 (该源非本人维护)
yay -Syu n-m3u8dl-re-git
```

---

## 命令行参数

```
Description:
  N_m3u8DL-RE (Beta version) 20251027

Usage:
  N_m3u8DL-RE <input> [options]

Arguments:
  <input>  链接或文件

Options:
  --tmp-dir <tmp-dir>                                     设置临时文件存储目录
  --save-dir <save-dir>                                   设置输出目录
  --save-name <save-name>                                 设置保存文件名
  --save-pattern <save-pattern>                           设置保存文件命名模板, 支持使用变量: 
                                                          <SaveName>, <Id>, <Codecs>, <Language>, <Resolution>, 
                                                          <Bandwidth>, <MediaType>, <Channels>, <FrameRate>, 
                                                          <VideoRange>, <GroupId>, <Ext>
                                                          示例: --save-pattern "<SaveName>_<Resolution>_<Bandwidth>"
  --log-file-path <log-file-path>                         设置日志文件路径, 例如 C:\Logs\log.txt
  --base-url <base-url>                                   设置BaseURL
  --thread-count <number>                                 设置下载线程数 [default: 本机CPU线程数]
  --download-retry-count <number>                         每个分片下载异常时的重试次数 [default: 3]
  --http-request-timeout <seconds>                        HTTP请求的超时时间(秒) [default: 100]
  --force-ansi-console                                    强制认定终端为支持ANSI且可交互的终端
  --no-ansi-color                                         去除ANSI颜色
  --auto-select                                           自动选择所有类型的最佳轨道 [default: False]
  --skip-merge                                            跳过合并分片 [default: False]
  --skip-download                                         跳过下载 [default: False]
  --check-segments-count                                  检测实际下载的分片数量和预期数量是否匹配 [default: True]
  --binary-merge                                          二进制合并 [default: False]
  --use-ffmpeg-concat-demuxer                             使用 ffmpeg 合并时,使用 concat 分离器而非 concat 协议 [default: False]
  --del-after-done                                        完成后删除临时文件 [default: True]
  --no-date-info                                          混流时不写入日期信息 [default: False]
  --no-log                                                关闭日志文件输出 [default: False]
  --write-meta-json                                       解析后的信息是否输出json文件 [default: True]
  --append-url-params                                     将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com [default: False]
  -mt, --concurrent-download                              并发下载已选择的音频、视频和字幕 [default: False]
  -H, --header <header>                                   为HTTP请求设置特定的请求头, 例如:
                                                          -H "Cookie: mycookie" -H "User-Agent: iOS"
  --sub-only                                              只选取字幕轨道 [default: False]
  --sub-format <SRT|VTT>                                  字幕输出类型 [default: SRT]
  --auto-subtitle-fix                                     自动修正字幕 [default: True]
  --ffmpeg-binary-path <PATH>                             ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe
  --log-level <DEBUG|ERROR|INFO|OFF|WARN>                 设置日志级别 [default: INFO]
  --ui-language <en-US|zh-CN|zh-TW>                       设置UI语言
  --urlprocessor-args <urlprocessor-args>                 此字符串将直接传递给URL Processor
  --key <key>                                             设置解密密钥, 程序调用mp4decrpyt/shaka-packager/ffmpeg进行解密. 格式:
                                                          --key KID1:KEY1 --key KID2:KEY2
                                                          对于KEY相同的情况可以直接输入 --key KEY
  --key-text-file <key-text-file>                         设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)
  --decryption-engine <FFMPEG|MP4DECRYPT|SHAKA_PACKAGER>  设置解密时使用的第三方程序 [default: MP4DECRYPT]
  --decryption-binary-path <PATH>                         MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe
  --mp4-real-time-decryption                              实时解密MP4分片 [default: False]
  -R, --max-speed <SPEED>                                 设置限速,单位支持 Mbps 或 Kbps,如:15M 100K
  -M, --mux-after-done <OPTIONS>                          所有工作完成时尝试混流分离的音视频. 输入 "--morehelp mux-after-done" 以查看详细信息
  --custom-hls-method <METHOD>                            指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)
  --custom-hls-key <FILE|HEX|BASE64>                      指定HLS解密KEY. 可以是文件, HEX或Base64
  --custom-hls-iv <FILE|HEX|BASE64>                       指定HLS解密IV. 可以是文件, HEX或Base64
  --use-system-proxy                                      使用系统默认代理 [default: True]
  --custom-proxy <URL>                                    设置请求代理, 如 http://127.0.0.1:8888
  --custom-range <RANGE>                                  仅下载部分分片. 输入 "--morehelp custom-range" 以查看详细信息
  --task-start-at <yyyyMMddHHmmss>                        在此时间之前不会开始执行任务
  --live-perform-as-vod                                   以点播方式下载直播流 [default: False]
  --live-real-time-merge                                  录制直播时实时合并 [default: False]
  --live-keep-segments                                    录制直播并开启实时合并时依然保留分片 [default: True]
  --live-pipe-mux                                         录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件 [default: False]
  --live-fix-vtt-by-audio                                 通过读取音频文件的起始时间修正VTT字幕 [default: False]
  --live-record-limit <HH:mm:ss>                          录制直播时的录制时长限制
  --live-wait-time <SEC>                                  手动设置直播列表刷新间隔
  --live-take-count <NUM>                                 手动设置录制直播时首次获取分片的数量 [default: 16]
  --mux-import <OPTIONS>                                  混流时引入外部媒体文件. 输入 "--morehelp mux-import" 以查看详细信息
  -sv, --select-video <OPTIONS>                           通过正则表达式选择符合要求的视频流. 输入 "--morehelp select-video" 以查看详细信息
  -sa, --select-audio <OPTIONS>                           通过正则表达式选择符合要求的音频流. 输入 "--morehelp select-audio" 以查看详细信息
  -ss, --select-subtitle <OPTIONS>                        通过正则表达式选择符合要求的字幕流. 输入 "--morehelp select-subtitle" 以查看详细信息
  -dv, --drop-video <OPTIONS>                             通过正则表达式去除符合要求的视频流.
  -da, --drop-audio <OPTIONS>                             通过正则表达式去除符合要求的音频流.
  -ds, --drop-subtitle <OPTIONS>                          通过正则表达式去除符合要求的字幕流.
  --ad-keyword <REG>                                      设置广告分片的URL关键字(正则表达式)
  --disable-update-check                                  禁用版本更新检测 [default: False]
  --allow-hls-multi-ext-map                               允许HLS中的多个#EXT-X-MAP(实验性) [default: False]
  --morehelp <OPTION>                                     查看某个选项的详细帮助信息
  -?, -h, --help                                          Show help and usage information
  --version                                               Show version information
```

<details>
<summary>点击查看More Help</summary>

```
More Help:

  --mux-after-done

所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:

* format=FORMAT: 指定混流容器 mkv, mp4
* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)
* bin_path=PATH: 指定程序路径 (默认: 自动寻找)
* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)
* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)

例如:
# 混流为mp4容器
-M format=mp4
# 使用mkvmerge, 自动寻找程序
-M format=mkv:muxer=mkvmerge
# 使用mkvmerge, 自定义程序路径
-M format=mkv:muxer=mkvmerge:bin_path="C\:\Program Files\MKVToolNix\mkvmerge.exe"
```

```
More Help:

  --mux-import

混流时引入外部媒体文件. 你能够以:分隔形式指定如下参数:

* path=PATH: 指定媒体文件路径
* lang=CODE: 指定媒体文件语言代码 (非必须)
* name=NAME: 指定媒体文件描述信息 (非必须)

例如:
# 引入外部字幕
--mux-import path=zh-Hans.srt:lang=chi:name="中文 (简体)"
# 引入外部音轨+字幕
--mux-import path="D\:\media\atmos.m4a":lang=eng:name="English Description Audio" --mux-import path="D\:\media\eng.vtt":lang=eng:name="English (Description)"
```

```
More Help:

  --select-video

通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数:

id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX
segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX
plistDurMin=hms:plistDurMax=hms:for=FOR

* for=FOR: 选择方式. best[number], worst[number], all (默认: best)

例如:
# 选择最佳视频
-sv best
# 选择4K+HEVC视频
-sv res="3840*":codecs=hvc1:for=best
# 选择长度大于1小时20分钟30秒的视频
-sv plistDurMin="1h20m30s":for=best
```

```
More Help:

  --select-audio

通过正则表达式选择符合要求的音频流. 参考 --select-video

例如:
# 选择所有音频
-sa all
# 选择最佳英语音轨
-sa lang=en:for=best
# 选择最佳的2条英语(或日语)音轨
-sa lang="ja|en":for=best2
```

```
More Help:

  --select-subtitle

通过正则表达式选择符合要求的字幕流. 参考 --select-video

例如:
# 选择所有字幕
-ss all
# 选择所有带有"中文"的字幕
-ss name="中文":for=all
```

```
More Help:

  --custom-range

下载点播内容时, 仅下载部分分片.

例如:
# 下载[0,10]共11个分片
--custom-range 0-10
# 下载从序号10开始的后续分片
--custom-range 10-
# 下载前100个分片
--custom-range -99
# 下载第5分钟到20分钟的内容
--custom-range 05:00-20:00
```
```
More Help:

  --save-pattern

使用变量设置输出文件命名模板. 支持的变量:

* <SaveName>: 用户指定的保存名称 (--save-name)
* <Id>: 流的任务ID
* <Codecs>: 编解码器信息 (例如: avc1.64001f, mp4a.40.2)
* <Language>: 语言代码 (例如: en, zh-CN)
* <Resolution>: 视频分辨率 (例如: 1920x1080)
* <Bandwidth>: 流的带宽/比特率
* <MediaType>: 媒体类型 (VIDEO, AUDIO, SUBTITLES)
* <Channels>: 音频声道配置
* <FrameRate>: 帧率
* <VideoRange>: 视频色域/HDR信息 (SDR, HDR10等)
* <GroupId>: 流组标识符

使用场景:
当下载多个相同类型的流时(例如多个不同分辨率的视频),使用此选项可以避免文件名冲突。

例如:
# 下载1080p和720p视频,文件名包含分辨率
--save-pattern "<SaveName>_<Resolution>" --save-name "video"
# 输出: video_1920x1080.mp4, video_1280x720.mp4

# 包含带宽信息
--save-pattern "<SaveName>_<Resolution>_<Bandwidth>kbps"
# 输出: video_1920x1080_5000000kbps.mp4

# 下载多个音频流,包含语言和声道
--save-pattern "<SaveName>_<Language>_<Channels>ch"
# 输出: audio_en_2ch.m4a, audio_es_2ch.m4a, audio_en_6ch.m4a

# 复杂模板
--save-pattern "<MediaType>_<Resolution>_<Codecs>_<Language>"
# 输出: VIDEO_1920x1080_avc1.64001f_en.mp4

注意:
如果不使用 --save-pattern,程序会在文件名冲突时自动使用流的元数据(分辨率、带宽等)
生成唯一的文件名,而不是简单地添加 ".copy" 后缀。
```

</details>

## 运行截图

### 点播

![RE1](img/RE.gif)

还可以并行下载+自动混流

![RE2](img/RE2.gif)

### 直播

录制TS直播源:

[click to show gif](http://pan.iqiyi.com/file/paopao/W0LfmaMRvuA--uCdOpZ1cldM5JCVhMfIm7KFqr4oKCz80jLn0bBb-9PWmeCFZ-qHpAaQydQ1zk-CHYT_UbRLtw.gif)

录制MPD直播源:

[click to show gif](http://pan.iqiyi.com/file/paopao/nmAV5MOh0yIyHhnxdgM_6th_p2nqrFsM4k-o3cUPwUa8Eh8QOU4uyPkLa_BlBrMa3GBnKWSk8rOaUwbsjKN14g.gif)

录制过程中,借助ffmpeg完成对音视频的实时混流

```
ffmpeg -readrate 1 -i 2022-09-21_19-54-42_V.mp4 -i 2022-09-21_19-54-42_V.chi.m4a -c copy 2022-09-21_19-54-42_V.ts
```

从 v0.1.5 开始,可以尝试开启 `live-pipe-mux` 来代替以上命令

> [!NOTE]
> 如果网络环境不够稳定,请不要开启 `live-pipe-mux`。管道内数据读取由 ffmpeg 负责,在某些环境下容易丢失直播数据。

从 v0.1.8 开始,能够通过设置环境变量 `RE_LIVE_PIPE_OPTIONS` 来改变 `live-pipe-mux` 时 ffmpeg 的某些选项: <https://github.com/nilaoda/N_m3u8DL-RE/issues/162#issuecomment-1592462532>

## 赞助

<a href="https://www.buymeacoffee.com/nilaoda" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>


================================================
FILE: TestStreams.md
================================================
# Test Streams

* https://play.itunes.apple.com/WebObjects/MZPlay.woa/hls/subscription/playlist.m3u8?cc=US&svcId=tvs.vds.4105&a=1580273278&isExternal=true&brandId=tvs.sbd.4000&id=337246031&l=en-US&aec=UHD&xtrick=true&webbrowser=true (啥都有)
* https://media.axprod.net/TestVectors/v7-Clear/Manifest_1080p.mpd (多音轨多字幕)
* https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd (直播)
* http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8 
* https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd
* https://dash.akamaized.net/dash264/TestCases/2c/qualcomm/1/MultiResMPEG2.mpd
* https://livesim.dashif.org/dash/vod/testpic_2s/multi_subs.mpd (ttml + mp4)
* http://media.axprod.net/TestVectors/v6-Clear/Manifest_1080p.mpd (vtt + mp4)
* https://livesim.dashif.org/dash/vod/testpic_2s/xml_subs.mpd (ttml)
* https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8 (HLS vtt)
* https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8 (高级HLS fMP4+VTT)
* https://events-delivery.apple.com/0205eyyhwbbqexozkwmgccegwnjyrktg/m3u8/vod_index-dpyfrsVksFWjneFiptbXnAMYBtGYbXeZ.m3u8 (高级HLS fMP4+VTT)
* https://a38avoddashs3ww-a.akamaihd.net/ondemand/iad_2/8e91/f2f2/ec5a/430f-bd7a-0779f4a0189d/685cda75-609c-41c1-86bb-688f4cdb5521_corrected.mpd
* https://theater.kktv.com.tw/98/04000198010001_584b26392f7f7f11fc62299214a55fb7/16113081449d8d5e9960_sub_dash.mpd (MPD+VTT)
* https://a38avoddashs3ww-a.akamaihd.net/ondemand/iad_2/8e91/f2f2/ec5a/430f-bd7a-0779f4a0189d/685cda75-609c-41c1-86bb-688f4cdb5521_corrected.mpd
* http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest
* http://amssamples.streaming.mediaservices.windows.net/683f7e47-bd83-4427-b0a3-26a6c4547782/BigBuckBunny.ism/manifest
* https://azclwds01.akamaized.net/4e8f6858-5d05-4e28-83ab-48c7a2b259e1/XVuosg_tab_hd.ism/Manifest (`e5114f87b3e54b9faca331e6e6d646a6:55c5d9f1cedfd018b75623f2565a1d29`)
* https://akm.eu.prd.media.max.com/bolt-glo-prod/78cccdce-2592-4a2b-a023-91034c43366e/packager-mp4-cenc/main.mpd (缺失Namespace)
* https://cdn01.vdocipher.com/media/6YI1GS6X5lAr7/b4550743/stream.mpd (单mp4链接 无法使用Bytes: 0-)

================================================
FILE: src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs
================================================
using N_m3u8DL_RE.Entity;
using Spectre.Console;
using Spectre.Console.Rendering;
using System.Collections.Concurrent;
using N_m3u8DL_RE.Common.Util;

namespace N_m3u8DL_RE.Column;

internal sealed class DownloadSpeedColumn : ProgressColumn
{
    private long _stopSpeed = 0;
    private ConcurrentDictionary<int, string> DateTimeStringDic = new();
    protected override bool NoWrap => true;
    private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }

    public DownloadSpeedColumn(ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic)
    {
        this.SpeedContainerDic = SpeedContainerDic;
    }

    public Style MyStyle { get; set; } = new Style(foreground: Color.Green);

    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
    {
        var taskId = task.Id;
        var speedContainer = SpeedContainerDic[taskId];
        var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        var flag = task.IsFinished || !task.IsStarted;
        // 单文件下载汇报进度
        if (!flag && speedContainer is { SingleSegment: true, ResponseLength: not null })
        {
            task.MaxValue = (double)speedContainer.ResponseLength;
            task.Value = speedContainer.RDownloaded;
        }
        // 一秒汇报一次即可
        if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
        {
            speedContainer.NowSpeed = speedContainer.Downloaded;
            // 速度为0,计数增加
            if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
            else speedContainer.ResetLowSpeedCount();
            speedContainer.Reset();
        }
        DateTimeStringDic[taskId] = now;
        var style = flag ? Style.Plain : MyStyle;
        return flag ? new Text("-", style).Centered() : new Text(GlobalUtil.FormatFileSize(speedContainer.NowSpeed) + "ps" + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered();
    }
}


================================================
FILE: src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs
================================================
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity;
using Spectre.Console;
using Spectre.Console.Rendering;
using System.Collections.Concurrent;

namespace N_m3u8DL_RE.Column;

internal class DownloadStatusColumn : ProgressColumn
{
    private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
    private ConcurrentDictionary<int, string> DateTimeStringDic = new();
    private ConcurrentDictionary<int, string> SizeDic = new();
    public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
    public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);

    public DownloadStatusColumn(ConcurrentDictionary<int, SpeedContainer> speedContainerDic)
    {
        this.SpeedContainerDic = speedContainerDic;
    }

    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
    {
        if (task.Value == 0) return new Text("-", MyStyle).RightJustified();
        var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");

        var speedContainer = SpeedContainerDic[task.Id];
        var size = speedContainer.RDownloaded;

        // 一秒汇报一次即可
        if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
        {
            var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
            SizeDic[task.Id] = $"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}";
        }
        DateTimeStringDic[task.Id] = now;
        SizeDic.TryGetValue(task.Id, out var sizeStr);

        if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);

        return new Text(sizeStr ?? "-", MyStyle).RightJustified();
    }
}

================================================
FILE: src/N_m3u8DL-RE/Column/MyPercentageColumn.cs
================================================
using Spectre.Console.Rendering;
using Spectre.Console;

namespace N_m3u8DL_RE.Column;

internal class MyPercentageColumn : ProgressColumn
{
    /// <summary>
    /// Gets or sets the style for a non-complete task.
    /// </summary>
    public Style Style { get; set; } = Style.Plain;

    /// <summary>
    /// Gets or sets the style for a completed task.
    /// </summary>
    public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);

    /// <inheritdoc/>
    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
    {
        var percentage = task.Percentage;
        var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
        return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
    }
}

================================================
FILE: src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs
================================================
using N_m3u8DL_RE.Common.Util;
using Spectre.Console;
using Spectre.Console.Rendering;
using System.Collections.Concurrent;

namespace N_m3u8DL_RE.Column;

internal class RecordingDurationColumn : ProgressColumn
{
    protected override bool NoWrap => true;
    private ConcurrentDictionary<int, int> _recodingDurDic;
    private ConcurrentDictionary<int, int>? _refreshedDurDic;
    public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey);
    public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen);
    public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic)
    {
        _recodingDurDic = recodingDurDic;
    }
    public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic, ConcurrentDictionary<int, int> refreshedDurDic)
    {
        _recodingDurDic = recodingDurDic;
        _refreshedDurDic = refreshedDurDic;
    }
    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
    {
        if (_refreshedDurDic == null)
            return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
        return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
    }
}

================================================
FILE: src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs
================================================
using N_m3u8DL_RE.Common.Util;
using Spectre.Console;
using Spectre.Console.Rendering;
using System.Collections.Concurrent;

namespace N_m3u8DL_RE.Column;

internal class RecordingSizeColumn : ProgressColumn
{
    protected override bool NoWrap => true;
    private ConcurrentDictionary<int, double> RecodingSizeDic = new(); // 临时的大小 每秒刷新用
    private ConcurrentDictionary<int, double> _recodingSizeDic;
    private ConcurrentDictionary<int, string> DateTimeStringDic = new();
    public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
    public RecordingSizeColumn(ConcurrentDictionary<int, double> recodingSizeDic)
    {
        _recodingSizeDic = recodingSizeDic;
    }
    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
    {
        var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        var taskId = task.Id;
        // 一秒汇报一次即可
        if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
        {
            RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
        }
        DateTimeStringDic[taskId] = now;
        var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
        return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
    }
}

================================================
FILE: src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs
================================================
using Spectre.Console;
using Spectre.Console.Rendering;

namespace N_m3u8DL_RE.Column;

internal class RecordingStatusColumn : ProgressColumn
{
    protected override bool NoWrap => true;
    public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
    public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
    {
        if (task.IsFinished)
            return new Text($"{task.Value}/{task.MaxValue} Waiting  ", FinishedStyle).LeftJustified();
        return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
    }
}

================================================
FILE: src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs
================================================
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Enum;
using N_m3u8DL_RE.Util;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;

namespace N_m3u8DL_RE.CommandLine;

internal static partial class CommandInvoker
{
    public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20251228";

    [GeneratedRegex("((best|worst)\\d*|all)")]
    private static partial Regex ForStrRegex();
    [GeneratedRegex(@"(\d*)-(\d*)")]
    private static partial Regex RangeRegex();
    [GeneratedRegex(@"([\d\\.]+)(M|K)")]
    private static partial Regex SpeedStrRegex();
    [GeneratedRegex("^[0-9a-fA-f]{32}:[0-9a-fA-f]{32}$")]
    private static partial Regex PairKeyRegex();
    [GeneratedRegex("^[0-9]{1,}:[0-9a-fA-f]{32}$")]
    private static partial Regex IdHexKeyRegex();
    [GeneratedRegex("^[0-9a-fA-f]{32}$")]
    private static partial Regex SingleHexKeyRegex();

    private static readonly Argument<string> Input = new("input") { Description = ResString.cmd_Input };
    private static readonly Option<string?> TmpDir = new("--tmp-dir") { Description = ResString.cmd_tmpDir };
    private static readonly Option<string?> SaveDir = new("--save-dir") { Description = ResString.cmd_saveDir };
    private static readonly Option<string?> SaveName = new("--save-name") { Description = ResString.cmd_saveName, CustomParser = ParseSaveName};
    private static readonly Option<string?> SavePattern = new("--save-pattern") { Description = ResString.cmd_savePattern };
    private static readonly Option<string?> LogFilePath = new("--log-file-path") { Description = ResString.cmd_logFilePath, CustomParser = ParseFilePath};
    private static readonly Option<string?> UILanguage = new Option<string?>("--ui-language") { Description = ResString.cmd_uiLanguage }.AcceptOnlyFromAmong("en-US", "zh-CN", "zh-TW");
    private static readonly Option<string?> UrlProcessorArgs = new("--urlprocessor-args") { Description = ResString.cmd_urlProcessorArgs };
    private static readonly Option<string> KeyTextFile = new("--key-text-file") { Description = ResString.cmd_keyText };
    private static readonly Option<Dictionary<string, string>> Headers = new("-H", "--header") { HelpName = "header", Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, Description = ResString.cmd_header, CustomParser = ParseHeaders };
    private static readonly Option<LogLevel> LogLevel = new("--log-level") { Description = ResString.cmd_logLevel, DefaultValueFactory = _ => Common.Log.LogLevel.INFO };
    private static readonly Option<SubtitleFormat> SubtitleFormat = new("--sub-format") { Description = ResString.cmd_subFormat, DefaultValueFactory = _ => Enum.SubtitleFormat.SRT };
    private static readonly Option<bool> DisableUpdateCheck = new Option<bool>("--disable-update-check") { Description = ResString.cmd_disableUpdateCheck }.WithDefault(false);
    private static readonly Option<bool> AutoSelect = new Option<bool>("--auto-select") { Description = ResString.cmd_autoSelect }.WithDefault(false);
    private static readonly Option<bool> SubOnly = new Option<bool>("--sub-only") { Description = ResString.cmd_subOnly }.WithDefault(false);
    private static readonly Option<int> ThreadCount = new("--thread-count") { HelpName = "number", Description = ResString.cmd_threadCount, DefaultValueFactory = _ => Environment.ProcessorCount };
    private static readonly Option<int> DownloadRetryCount = new("--download-retry-count") { HelpName = "number", Description = ResString.cmd_downloadRetryCount, DefaultValueFactory = _ => 3 };
    private static readonly Option<double> HttpRequestTimeout = new("--http-request-timeout") { HelpName = "seconds", Description = ResString.cmd_httpRequestTimeout, DefaultValueFactory = _ => 100 };
    private static readonly Option<bool> SkipMerge = new Option<bool>("--skip-merge") { Description = ResString.cmd_skipMerge }.WithDefault(false);
    private static readonly Option<bool> SkipDownload = new Option<bool>("--skip-download") { Description = ResString.cmd_skipDownload }.WithDefault(false);
    private static readonly Option<bool> NoDateInfo = new Option<bool>("--no-date-info") { Description = ResString.cmd_noDateInfo }.WithDefault(false);
    private static readonly Option<bool> BinaryMerge = new Option<bool>("--binary-merge") { Description = ResString.cmd_binaryMerge }.WithDefault(false);
    private static readonly Option<bool> UseFFmpegConcatDemuxer = new Option<bool>("--use-ffmpeg-concat-demuxer") { Description = ResString.cmd_useFFmpegConcatDemuxer }.WithDefault(false);
    private static readonly Option<bool> DelAfterDone = new Option<bool>("--del-after-done") { Description = ResString.cmd_delAfterDone }.WithDefault(true);
    private static readonly Option<bool> AutoSubtitleFix = new Option<bool>("--auto-subtitle-fix") { Description = ResString.cmd_subtitleFix }.WithDefault(true);
    private static readonly Option<bool> CheckSegmentsCount = new Option<bool>("--check-segments-count") { Description = ResString.cmd_checkSegmentsCount }.WithDefault(true);
    private static readonly Option<bool> WriteMetaJson = new Option<bool>("--write-meta-json") { Description = ResString.cmd_writeMetaJson }.WithDefault(true);
    private static readonly Option<bool> AppendUrlParams = new Option<bool>("--append-url-params") { Description = ResString.cmd_appendUrlParams }.WithDefault(false);
    private static readonly Option<bool> MP4RealTimeDecryption = new Option<bool>("--mp4-real-time-decryption") { Description = ResString.cmd_MP4RealTimeDecryption }.WithDefault(false);
    private static readonly Option<bool> UseShakaPackager = new Option<bool>("--use-shaka-packager") { Hidden = true, Description = ResString.cmd_useShakaPackager }.WithDefault(false);
    private static readonly Option<DecryptEngine> DecryptionEngine = new ("--decryption-engine") { Description = ResString.cmd_decryptionEngine, DefaultValueFactory = _ => DecryptEngine.MP4DECRYPT };
    private static readonly Option<bool> ForceAnsiConsole = new("--force-ansi-console") { Description = ResString.cmd_forceAnsiConsole };
    private static readonly Option<bool> NoAnsiColor = new("--no-ansi-color") { Description = ResString.cmd_noAnsiColor };
    private static readonly Option<string?> DecryptionBinaryPath = new("--decryption-binary-path") { HelpName = "PATH", Description = ResString.cmd_decryptionBinaryPath };
    private static readonly Option<string?> FFmpegBinaryPath = new("--ffmpeg-binary-path") { HelpName = "PATH", Description = ResString.cmd_ffmpegBinaryPath };
    private static readonly Option<string?> BaseUrl = new("--base-url") { Description = ResString.cmd_baseUrl };
    private static readonly Option<bool> ConcurrentDownload = new Option<bool>("-mt", "--concurrent-download") { Description = ResString.cmd_concurrentDownload }.WithDefault(false);
    private static readonly Option<bool> NoLog = new Option<bool>("--no-log") { Description = ResString.cmd_noLog }.WithDefault(false);
    private static readonly Option<bool> AllowHlsMultiExtMap = new Option<bool>("--allow-hls-multi-ext-map") { Description = ResString.cmd_allowHlsMultiExtMap }.WithDefault(false);
    private static readonly Option<string[]?> AdKeywords = new("--ad-keyword") { HelpName = "REG", Description = ResString.cmd_adKeyword };
    private static readonly Option<long?> MaxSpeed = new("-R", "--max-speed") { HelpName = "SPEED", Description = ResString.cmd_maxSpeed, CustomParser = ParseSpeedLimit };


    // 代理选项
    private static readonly Option<bool> UseSystemProxy = new Option<bool>("--use-system-proxy") { Description = ResString.cmd_useSystemProxy }.WithDefault(true);
    private static readonly Option<WebProxy?> CustomProxy = new("--custom-proxy") { HelpName = "URL", Description = ResString.cmd_customProxy, CustomParser = ParseProxy};

    // 只下载部分分片
    private static readonly Option<CustomRange?> CustomRange = new("--custom-range") { HelpName = "RANGE", Description = ResString.cmd_customRange, CustomParser = ParseCustomRange };


    // morehelp
    private static readonly Option<string?> MoreHelp = new("--morehelp") { HelpName = "OPTION", Description = ResString.cmd_moreHelp };

    // 自定义KEY等
    private static readonly Option<EncryptMethod?> CustomHLSMethod = new("--custom-hls-method") { HelpName = "METHOD", Description = ResString.cmd_customHLSMethod };
    private static readonly Option<byte[]?> CustomHLSKey = new("--custom-hls-key") { HelpName = "FILE|HEX|BASE64", Description = ResString.cmd_customHLSKey, CustomParser = ParseHLSCustomKey };
    private static readonly Option<byte[]?> CustomHLSIv = new(name: "--custom-hls-iv") { HelpName = "FILE|HEX|BASE64", Description = ResString.cmd_customHLSIv, CustomParser = ParseHLSCustomKey };
    private static readonly Option<string[]?> Keys = new("--key") { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, Description = ResString.cmd_keys, CustomParser = ParseCustomKeys};

    // 任务开始时间
    private static readonly Option<DateTime?> TaskStartAt = new("--task-start-at") { HelpName = "yyyyMMddHHmmss", Description = ResString.cmd_taskStartAt, CustomParser = ParseStartTime };


    // 直播相关
    private static readonly Option<bool> LivePerformAsVod = new Option<bool>("--live-perform-as-vod") { Description = ResString.cmd_livePerformAsVod }.WithDefault(false);
    private static readonly Option<bool> LiveRealTimeMerge = new Option<bool>("--live-real-time-merge") { Description = ResString.cmd_liveRealTimeMerge }.WithDefault(false);
    private static readonly Option<bool> LiveKeepSegments = new Option<bool>("--live-keep-segments") { Description = ResString.cmd_liveKeepSegments }.WithDefault(true);
    private static readonly Option<bool> LivePipeMux = new Option<bool>("--live-pipe-mux") { Description = ResString.cmd_livePipeMux }.WithDefault(false);
    private static readonly Option<TimeSpan?> LiveRecordLimit = new("--live-record-limit") { HelpName = "HH:mm:ss", Description = ResString.cmd_liveRecordLimit, CustomParser = ParseLiveLimit };
    private static readonly Option<int?> LiveWaitTime = new("--live-wait-time") { HelpName = "SEC", Description = ResString.cmd_liveWaitTime };
    private static readonly Option<int> LiveTakeCount = new("--live-take-count") { HelpName = "NUM", Description = ResString.cmd_liveTakeCount, DefaultValueFactory = _ => 16 };
    private static readonly Option<bool> LiveFixVttByAudio = new Option<bool>("--live-fix-vtt-by-audio") { Description = ResString.cmd_liveFixVttByAudio }.WithDefault(false);


    // 复杂命令行如下
    private static readonly Option<MuxOptions?> MuxAfterDone = new("-M", "--mux-after-done") { HelpName = "OPTIONS", Description = ResString.cmd_muxAfterDone, CustomParser = ParseMuxAfterDone };
    private static readonly Option<List<OutputFile>> MuxImports = new("--mux-import") { HelpName = "OPTIONS", Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, Description = ResString.cmd_muxImport, CustomParser = ParseImports };
    private static readonly Option<StreamFilter?> VideoFilter = new("-sv", "--select-video") { HelpName = "OPTIONS", Description = ResString.cmd_selectVideo, CustomParser = ParseStreamFilter };
    private static readonly Option<StreamFilter?> AudioFilter = new("-sa", "--select-audio") { HelpName = "OPTIONS", Description = ResString.cmd_selectAudio, CustomParser = ParseStreamFilter };
    private static readonly Option<StreamFilter?> SubtitleFilter = new("-ss", "--select-subtitle") { HelpName = "OPTIONS", Description = ResString.cmd_selectSubtitle, CustomParser = ParseStreamFilter };

    private static readonly Option<StreamFilter?> DropVideoFilter = new("-dv", "--drop-video") { HelpName = "OPTIONS", Description = ResString.cmd_dropVideo, CustomParser = ParseStreamFilter };
    private static readonly Option<StreamFilter?> DropAudioFilter = new("-da", "--drop-audio") { HelpName = "OPTIONS", Description = ResString.cmd_dropAudio, CustomParser = ParseStreamFilter };
    private static readonly Option<StreamFilter?> DropSubtitleFilter = new("-ds", "--drop-subtitle") { HelpName = "OPTIONS", Description = ResString.cmd_dropSubtitle, CustomParser = ParseStreamFilter };

    /// <summary>
    /// 解析下载速度限制
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static long? ParseSpeedLimit(ArgumentResult result)
    {
        var input = result.Tokens[0].Value.ToUpper();
        try
        {
            var reg = SpeedStrRegex();
            if (!reg.IsMatch(input)) throw new ArgumentException($"Invalid Speed Limit: {input}");

            var number = double.Parse(reg.Match(input).Groups[1].Value);
            if (reg.Match(input).Groups[2].Value == "M")
                return (long)(number * 1024 * 1024);
            return (long)(number * 1024);
        }
        catch (Exception)
        {
            result.AddError("error in parse SpeedLimit: " + input);
            return null;
        }
    }

    /// <summary>
    /// 解析用户定义的下载范围
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentException"></exception>
    private static CustomRange? ParseCustomRange(ArgumentResult result)
    {
        var input = result.Tokens[0].Value;
        // 支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00;
        try
        {
            if (string.IsNullOrEmpty(input))
                return null;

            var arr = input.Split('-');
            if (arr.Length != 2)
                throw new ArgumentException("Bad format!");

            if (input.Contains(':'))
            {
                return new CustomRange()
                {
                    InputStr = input,
                    StartSec = arr[0] == "" ? 0 : OtherUtil.ParseDur(arr[0]).TotalSeconds,
                    EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds,
                };
            }

            if (RangeRegex().IsMatch(input))
            {
                var left = RangeRegex().Match(input).Groups[1].Value;
                var right = RangeRegex().Match(input).Groups[2].Value;
                return new CustomRange()
                {
                    InputStr = input,
                    StartSegIndex = left == "" ? 0 : long.Parse(left),
                    EndSegIndex = right == "" ? long.MaxValue : long.Parse(right),
                };
            }

            throw new ArgumentException("Bad format!");
        }
        catch (Exception ex)
        {
            result.AddError("error in parse CustomRange: " + ex.Message);
            return null;
        }
    }

    /// <summary>
    /// 解析用户代理
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentException"></exception>
    private static WebProxy? ParseProxy(ArgumentResult result)
    {
        var input = result.Tokens[0].Value;
        try
        {
            if (string.IsNullOrEmpty(input))
                return null;

            var uri = new Uri(input);
            var proxy = new WebProxy(uri, true);
            if (!string.IsNullOrEmpty(uri.UserInfo))
            {
                var infos = uri.UserInfo.Split(':');
                proxy.Credentials = new NetworkCredential(infos.First(), infos.Last());
            }
            return proxy;
        }
        catch (Exception ex)
        {
            result.AddError("error in parse proxy: " + ex.Message);
            return null;
        }
    }

    /// <summary>
    /// 解析自定义KEY(用于mp4decrypt等第三方程序)
    /// 支持格式:<br/>
    /// - KEY(hex)<br/>
    /// - KID:KEY(hex)<br/>
    /// - Base64KEY<br/>
    /// - Base64KID:Base64KEY
    /// </summary>
    private static string[]? ParseCustomKeys(ArgumentResult result)
    {
        const int KeyBytes = 16;
        const int KeyHexLen = KeyBytes * 2;
        
        string ParsePart(string part, string label)
        {
            if (SingleHexKeyRegex().IsMatch(part))
                return part.ToLowerInvariant();

            if (HexUtil.TryParseBase64(part, out var hex) && hex is { Length: KeyHexLen })
                return hex.ToLowerInvariant();

            throw new ArgumentException($"{label} must be valid 16-byte HEX or Base64. Input string: {part}");
        }

        var keys = new List<string>();
        var inputs = result.Tokens.Select(t => t.Value).ToList();

        try
        {
            foreach (var input in inputs)
            {
                // 已匹配标准格式的,直接添加
                if (PairKeyRegex().IsMatch(input) || IdHexKeyRegex().IsMatch(input) || SingleHexKeyRegex().IsMatch(input))
                {
                    keys.Add(input);
                    continue;
                }

                // 拆分KID:KEY
                var parts = input.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

                if (parts.Length is < 1 or > 2)
                    throw new ArgumentException("Input must be KEY or KID:KEY format.");

                if (parts.Length == 1)
                {
                    var key = ParsePart(parts[0], "KEY");
                    keys.Add(key);
                }
                else // KID:KEY
                {
                    var kid = ParsePart(parts[0], "KID");
                    var key = ParsePart(parts[1], "KEY");
                    keys.Add($"{kid}:{key}");
                }
            }

            return [.. keys];
        }
        catch (Exception ex)
        {
            result.AddError($"error in parse custom key: {ex.Message}. All Inputs=[{string.Join(", ", inputs)}]");
            return null;
        }
    }

    /// <summary>
    /// 解析自定义KEY
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static byte[]? ParseHLSCustomKey(ArgumentResult result)
    {
        var input = result.Tokens[0].Value;
        try
        {
            if (string.IsNullOrEmpty(input))
                return null;
            if (File.Exists(input))
                return File.ReadAllBytes(input);
            if (HexUtil.TryParseHexString(input, out byte[]? bytes))
                return bytes;
            return Convert.FromBase64String(input);
        }
        catch (Exception)
        {
            result.AddError("error in parse hls custom key: " + input);
            return null;
        }
    }

    /// <summary>
    /// 解析录制直播时长限制
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static TimeSpan? ParseLiveLimit(ArgumentResult result)
    {
        var input = result.Tokens[0].Value;
        try
        {
            return OtherUtil.ParseDur(input);
        }
        catch (Exception)
        {
            result.AddError("error in parse LiveRecordLimit: " + input);
            return null;
        }
    }

    /// <summary>
    /// 解析任务开始时间
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static DateTime? ParseStartTime(ArgumentResult result)
    {
        var input = result.Tokens[0].Value;
        try
        {
            CultureInfo provider = CultureInfo.InvariantCulture;
            return DateTime.ParseExact(input, "yyyyMMddHHmmss", provider);
        }
        catch (Exception)
        {
            result.AddError("error in parse TaskStartTime: " + input);
            return null;
        }
    }

    private static string? ParseSaveName(ArgumentResult result)
    {
        var input = result.Tokens[0].Value;
        var newName = OtherUtil.GetValidFileName(input);
        if (string.IsNullOrEmpty(newName))
        {
            result.AddError("Invalid save name!");
            return null;
        }
        return newName;
    }

    private static string? ParseFilePath(ArgumentResult result)
    {
        var input = result.Tokens[0].Value;
        var path = "";
        try
        {
            path = Path.GetFullPath(input);
        }
        catch (Exception e)
        {
            result.AddError("Invalid log path!");
            return null;
        }
        var dir = Path.GetDirectoryName(path);
        var filename = Path.GetFileName(path);
        var newName = OtherUtil.GetValidFileName(filename);
        if (string.IsNullOrEmpty(newName))
        {
            result.AddError("Invalid log file name!");
            return null;
        }
        return Path.Combine(dir!, newName);
    }

    /// <summary>
    /// 流过滤器
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static StreamFilter? ParseStreamFilter(ArgumentResult result)
    {
        var streamFilter = new StreamFilter();
        var input = result.Tokens[0].Value;
        var p = new ComplexParamParser(input);


        // 目标范围
        var forStr = "";
        if (input == ForStrRegex().Match(input).Value)
        {
            forStr = input;
        }
        else
        {
            forStr = p.GetValue("for") ?? "best";
            if (forStr != ForStrRegex().Match(forStr).Value)
            {
                result.AddError($"for={forStr} not valid");
                return null;
            }
        }
        streamFilter.For = forStr;

        var id = p.GetValue("id");
        if (!string.IsNullOrEmpty(id))
            streamFilter.GroupIdReg = new Regex(id);

        var lang = p.GetValue("lang");
        if (!string.IsNullOrEmpty(lang))
            streamFilter.LanguageReg = new Regex(lang);

        var name = p.GetValue("name");
        if (!string.IsNullOrEmpty(name))
            streamFilter.NameReg = new Regex(name);

        var codecs = p.GetValue("codecs");
        if (!string.IsNullOrEmpty(codecs))
            streamFilter.CodecsReg = new Regex(codecs);

        var res = p.GetValue("res");
        if (!string.IsNullOrEmpty(res))
            streamFilter.ResolutionReg = new Regex(res);

        var frame = p.GetValue("frame");
        if (!string.IsNullOrEmpty(frame))
            streamFilter.FrameRateReg = new Regex(frame);

        var channel = p.GetValue("channel");
        if (!string.IsNullOrEmpty(channel))
            streamFilter.ChannelsReg = new Regex(channel);

        var range = p.GetValue("range");
        if (!string.IsNullOrEmpty(range))
            streamFilter.VideoRangeReg = new Regex(range);

        var url = p.GetValue("url");
        if (!string.IsNullOrEmpty(url))
            streamFilter.UrlReg = new Regex(url);

        var segsMin = p.GetValue("segsMin");
        if (!string.IsNullOrEmpty(segsMin))
            streamFilter.SegmentsMinCount = long.Parse(segsMin);

        var segsMax = p.GetValue("segsMax");
        if (!string.IsNullOrEmpty(segsMax))
            streamFilter.SegmentsMaxCount = long.Parse(segsMax);

        var plistDurMin = p.GetValue("plistDurMin");
        if (!string.IsNullOrEmpty(plistDurMin))
            streamFilter.PlaylistMinDur = OtherUtil.ParseSeconds(plistDurMin);

        var plistDurMax = p.GetValue("plistDurMax");
        if (!string.IsNullOrEmpty(plistDurMax))
            streamFilter.PlaylistMaxDur = OtherUtil.ParseSeconds(plistDurMax);

        var bwMin = p.GetValue("bwMin");
        if (!string.IsNullOrEmpty(bwMin))
            streamFilter.BandwidthMin = int.Parse(bwMin) * 1000;

        var bwMax = p.GetValue("bwMax");
        if (!string.IsNullOrEmpty(bwMax))
            streamFilter.BandwidthMax = int.Parse(bwMax) * 1000;

        var role = p.GetValue("role");
        if (System.Enum.TryParse(role, true, out RoleType roleType))
            streamFilter.Role = roleType;

        return streamFilter;
    }

    /// <summary>
    /// 分割Header
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static Dictionary<string, string> ParseHeaders(ArgumentResult result)
    {
        var array = result.Tokens.Select(t => t.Value).ToArray();
        return OtherUtil.SplitHeaderArrayToDic(array);
    }

    /// <summary>
    /// 解析混流引入的外部文件
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static List<OutputFile> ParseImports(ArgumentResult result)
    {
        var imports = new List<OutputFile>();

        foreach (var item in result.Tokens)
        {
            var p = new ComplexParamParser(item.Value);
            var path = p.GetValue("path") ?? item.Value; // 若未获取到,直接整个字符串作为path
            var lang = p.GetValue("lang");
            var name = p.GetValue("name");
            if (string.IsNullOrEmpty(path) || !File.Exists(path))
            {
                result.AddError("path empty or file not exists!");
                return imports;
            }
            imports.Add(new OutputFile()
            {
                Index = 999,
                FilePath = path,
                LangCode = lang,
                Description = name
            });
        }

        return imports;
    }

    /// <summary>
    /// 解析混流选项
    /// </summary>
    /// <param name="result"></param>
    /// <returns></returns>
    private static MuxOptions? ParseMuxAfterDone(ArgumentResult result)
    {
        var v = result.Tokens[0].Value;
        var p = new ComplexParamParser(v);
        // 混流格式
        var format = p.GetValue("format") ?? v.Split(':')[0]; // 若未获取到,直接:前的字符串作为format解析
        var parseResult = System.Enum.TryParse(format.ToUpperInvariant(), out MuxFormat muxFormat);
        if (!parseResult)
        {
            result.AddError($"format={format} not valid");
            return null;
        }
        // 混流器
        var muxer = p.GetValue("muxer") ?? "ffmpeg";
        if (muxer != "ffmpeg" && muxer != "mkvmerge")
        {
            result.AddError($"muxer={muxer} not valid");
            return null;
        }
        // 混流器路径
        var bin_path = p.GetValue("bin_path") ?? "auto";
        if (string.IsNullOrEmpty(bin_path))
        {
            result.AddError($"bin_path={bin_path} not valid");
            return null;
        }
        // 是否删除
        var keep = p.GetValue("keep") ?? "false";
        if (keep != "true" && keep != "false")
        {
            result.AddError($"keep={keep} not valid");
            return null;
        }
        // 是否忽略字幕
        var skipSub = p.GetValue("skip_sub") ?? "false";
        if (skipSub != "true" && skipSub != "false")
        {
            result.AddError($"skip_sub={keep} not valid");
            return null;
        }
        // 冲突检测
        if (muxer == "mkvmerge" && format == "mp4")
        {
            result.AddError($"mkvmerge can not do mp4");
            return null;
        }
        return new MuxOptions()
        {
            UseMkvmerge = muxer == "mkvmerge",
            MuxFormat = muxFormat,
            KeepFiles = keep == "true",
            SkipSubtitle = skipSub == "true",
            BinPath = bin_path == "auto" ? null : bin_path
        };
    }

    private static bool HasOption(this ParseResult result, Option option)
    {
        var allTokens = result.Tokens.Select(x => x.Value).ToList();
        List<string> optionNames = [option.Name, ..option.Aliases];
        return optionNames.Any(x => allTokens.Contains(x));
    }
    
    private static Option<T> WithDefault<T>(this Option<T> option, T defaultValue)
    {
        if (option is not Option<bool>)
            return option;
        option.DefaultValueFactory = _ => defaultValue;
        var currentDesc = option.Description ?? string.Empty;
        var defaultText = defaultValue?.ToString() ?? "null";
        // 拼接:原描述 + 空格 + [default: ...]
        option.Description = string.IsNullOrWhiteSpace(currentDesc)
            ? $"[default: {defaultText}]"
            : $"{currentDesc.Trim()} [default: {defaultText}]";
        return option;
    }

    private static MyOption GetOptions(ParseResult result)
    {
        var option = new MyOption
        {
            Input = result.GetRequiredValue(Input),
            ForceAnsiConsole = result.GetValue(ForceAnsiConsole),
            NoAnsiColor = result.GetValue(NoAnsiColor),
            LogLevel = result.GetValue(LogLevel),
            AutoSelect = result.GetValue(AutoSelect),
            DisableUpdateCheck = result.GetValue(DisableUpdateCheck),
            SkipMerge = result.GetValue(SkipMerge),
            BinaryMerge = result.GetValue(BinaryMerge),
            UseFFmpegConcatDemuxer = result.GetValue(UseFFmpegConcatDemuxer),
            DelAfterDone = result.GetValue(DelAfterDone),
            AutoSubtitleFix = result.GetValue(AutoSubtitleFix),
            CheckSegmentsCount = result.GetValue(CheckSegmentsCount),
            SubtitleFormat = result.GetValue(SubtitleFormat),
            SubOnly = result.GetValue(SubOnly),
            TmpDir = result.GetValue(TmpDir),
            SaveDir = result.GetValue(SaveDir),
            SaveName = result.GetValue(SaveName),
            LogFilePath = result.GetValue(LogFilePath),
            ThreadCount = result.GetValue(ThreadCount),
            UILanguage = result.GetValue(UILanguage),
            SkipDownload = result.GetValue(SkipDownload),
            WriteMetaJson = result.GetValue(WriteMetaJson),
            AppendUrlParams = result.GetValue(AppendUrlParams),
            SavePattern = result.GetValue(SavePattern),
            Keys = result.GetValue(Keys),
            UrlProcessorArgs = result.GetValue(UrlProcessorArgs),
            MP4RealTimeDecryption = result.GetValue(MP4RealTimeDecryption),
            UseShakaPackager = result.GetValue(UseShakaPackager),
            DecryptionEngine = result.GetValue(DecryptionEngine),
            DecryptionBinaryPath = result.GetValue(DecryptionBinaryPath),
            FFmpegBinaryPath = result.GetValue(FFmpegBinaryPath),
            KeyTextFile = result.GetValue(KeyTextFile),
            DownloadRetryCount = result.GetValue(DownloadRetryCount),
            HttpRequestTimeout = result.GetValue(HttpRequestTimeout),
            BaseUrl = result.GetValue(BaseUrl),
            MuxImports = result.GetValue(MuxImports),
            ConcurrentDownload = result.GetValue(ConcurrentDownload),
            VideoFilter = result.GetValue(VideoFilter),
            AudioFilter = result.GetValue(AudioFilter),
            SubtitleFilter = result.GetValue(SubtitleFilter),
            DropVideoFilter = result.GetValue(DropVideoFilter),
            DropAudioFilter = result.GetValue(DropAudioFilter),
            DropSubtitleFilter = result.GetValue(DropSubtitleFilter),
            LiveRealTimeMerge = result.GetValue(LiveRealTimeMerge),
            LiveKeepSegments = result.GetValue(LiveKeepSegments),
            LiveRecordLimit = result.GetValue(LiveRecordLimit),
            TaskStartAt = result.GetValue(TaskStartAt),
            LivePerformAsVod = result.GetValue(LivePerformAsVod),
            LivePipeMux = result.GetValue(LivePipeMux),
            LiveFixVttByAudio = result.GetValue(LiveFixVttByAudio),
            UseSystemProxy = result.GetValue(UseSystemProxy),
            CustomProxy = result.GetValue(CustomProxy),
            CustomRange = result.GetValue(CustomRange),
            LiveWaitTime = result.GetValue(LiveWaitTime),
            LiveTakeCount = result.GetValue(LiveTakeCount),
            NoDateInfo = result.GetValue(NoDateInfo),
            NoLog = result.GetValue(NoLog),
            AllowHlsMultiExtMap = result.GetValue(AllowHlsMultiExtMap),
            AdKeywords = result.GetValue(AdKeywords),
            MaxSpeed = result.GetValue(MaxSpeed),
        };

        if (result.HasOption(CustomHLSMethod)) option.CustomHLSMethod = result.GetValue(CustomHLSMethod);
        if (result.HasOption(CustomHLSKey)) option.CustomHLSKey = result.GetValue(CustomHLSKey);
        if (result.HasOption(CustomHLSIv)) option.CustomHLSIv = result.GetValue(CustomHLSIv);

        var parsedHeaders = result.GetValue(Headers);
        if (parsedHeaders != null)
            option.Headers = parsedHeaders;


        // 以用户选择语言为准优先
        if (option.UILanguage != null)
        {
            CultureUtil.ChangeCurrentCultureName(option.UILanguage);
        }

        // 混流设置
        var muxAfterDoneValue = result.GetValue(MuxAfterDone);
        if (muxAfterDoneValue == null) return option;
        
        option.MuxAfterDone = true;
        option.MuxOptions = muxAfterDoneValue;
        if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath;
        else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath;

        return option;
    }


    public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> action)
    {
        var argList = new List<string>(args);
        var index = -1;
        if ((index = argList.IndexOf("--morehelp")) >= 0 && argList.Count > index + 1)
        {
            var option = argList[index + 1];
            var msg = option switch
            {
                "mux-after-done" => ResString.cmd_muxAfterDone_more,
                "mux-import" => ResString.cmd_muxImport_more,
                "select-video" => ResString.cmd_selectVideo_more,
                "select-audio" => ResString.cmd_selectAudio_more,
                "select-subtitle" => ResString.cmd_selectSubtitle_more,
                "custom-range" => ResString.cmd_custom_range,
                _ => $"Option=\"{option}\" not found"
            };
            Console.WriteLine($"More Help:\r\n\r\n  --{option}\r\n\r\n" + msg);
            Environment.Exit(0);
        }

        var rootCommand = new RootCommand(VERSION_INFO)
        {
            Input, TmpDir, SaveDir, SaveName, SavePattern, LogFilePath, BaseUrl, ThreadCount, DownloadRetryCount, HttpRequestTimeout, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,
            BinaryMerge, UseFFmpegConcatDemuxer, DelAfterDone, NoDateInfo, NoLog, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, SubOnly, SubtitleFormat, AutoSubtitleFix,
            FFmpegBinaryPath,
            LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionEngine, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption,
            MaxSpeed,
            MuxAfterDone,
            CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, CustomRange, TaskStartAt,
            LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveFixVttByAudio, LiveRecordLimit, LiveWaitTime, LiveTakeCount,
            MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, AdKeywords, DisableUpdateCheck, AllowHlsMultiExtMap, MoreHelp
        };

        rootCommand.TreatUnmatchedTokensAsErrors = true;
        rootCommand.SetAction(parseResult =>
        {
            var myOption = GetOptions(parseResult);
            return action(myOption);
        });

        var config = new ParserConfiguration
        {
            EnablePosixBundling = false
        };

        try
        {
            var parseResult = rootCommand.Parse(args, config);
            var exitCode = await parseResult.InvokeAsync();
            Environment.Exit(exitCode);
        }
        catch (Exception ex)
        {
            var msg = Logger.LogLevel == Common.Log.LogLevel.DEBUG 
                ? ex.ToString() 
                : ex.Message;
#if DEBUG
            msg = ex.ToString();
#endif
            Logger.Error(msg);
            Thread.Sleep(3000);
            Environment.Exit(1);
        }
        finally
        {
            try { Console.CursorVisible = true; } catch { }
        }

        return 0;
    }
}


================================================
FILE: src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs
================================================
using System.Text;

namespace N_m3u8DL_RE.CommandLine;

internal class ComplexParamParser
{
    private readonly string _arg;
    public ComplexParamParser(string arg)
    {
        _arg = arg;
    }

    public string? GetValue(string key)
    {
        if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(_arg)) return null;

        try
        {
            var index = _arg.IndexOf(key + "=", StringComparison.Ordinal);
            if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;

            var chars = _arg[(index + key.Length + 1)..].ToCharArray();
            var result = new StringBuilder();
            char last = '\0';
            for (int i = 0; i < chars.Length; i++)
            {
                if (chars[i] == ':')
                {
                    if (last == '\\')
                    {
                        result.Replace("\\", "");
                        last = chars[i];
                        result.Append(chars[i]);
                    }
                    else break;
                }
                else
                {
                    last = chars[i];
                    result.Append(chars[i]);
                }
            }

            var resultStr = result.ToString().Trim().Trim('\"').Trim('\'');

            // 不应该有引号出现
            if (resultStr.Contains('\"') || resultStr.Contains('\'')) throw new Exception();

            return resultStr;
        }
        catch (Exception)
        {
            throw new ArgumentException($"Parse Argument [{key}] failed!");
        }
    }
}

================================================
FILE: src/N_m3u8DL-RE/CommandLine/MyOption.cs
================================================
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Enum;
using System.Net;

namespace N_m3u8DL_RE.CommandLine;

internal class MyOption
{
    /// <summary>
    /// See: <see cref="CommandInvoker.Input"/>.
    /// </summary>
    public string Input { get; set; } = default!;
    /// <summary>
    /// See: <see cref="CommandInvoker.Headers"/>.
    /// </summary>
    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
    /// <summary>
    /// See: <see cref="CommandInvoker.AdKeywords"/>.
    /// </summary>
    public string[]? AdKeywords { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.MaxSpeed"/>.
    /// </summary>
    public long? MaxSpeed { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.Keys"/>.
    /// </summary>
    public string[]? Keys { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.BaseUrl"/>.
    /// </summary>
    public string? BaseUrl { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.KeyTextFile"/>.
    /// </summary>
    public string? KeyTextFile { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.UrlProcessorArgs"/>.
    /// </summary>
    public string? UrlProcessorArgs { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LogLevel"/>.
    /// </summary>
    public LogLevel LogLevel { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.NoDateInfo"/>.
    /// </summary>
    public bool NoDateInfo { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.NoLog"/>.
    /// </summary>
    public bool NoLog { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.AllowHlsMultiExtMap"/>.
    /// </summary>
    public bool AllowHlsMultiExtMap { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.AutoSelect"/>.
    /// </summary>
    public bool AutoSelect { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DisableUpdateCheck"/>.
    /// </summary>
    public bool DisableUpdateCheck { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SubOnly"/>.
    /// </summary>
    public bool SubOnly { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.ThreadCount"/>.
    /// </summary>
    public int ThreadCount { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DownloadRetryCount"/>.
    /// </summary>
    public int DownloadRetryCount { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.HttpRequestTimeout"/>.
    /// </summary>
    public double HttpRequestTimeout { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LiveRecordLimit"/>.
    /// </summary>
    public TimeSpan? LiveRecordLimit { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.TaskStartAt"/>.
    /// </summary>
    public DateTime? TaskStartAt { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SkipMerge"/>.
    /// </summary>
    public bool SkipMerge { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.BinaryMerge"/>.
    /// </summary>
    public bool BinaryMerge { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.ForceAnsiConsole"/>.
    /// </summary>
    public bool ForceAnsiConsole { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.NoAnsiColor"/>.
    /// </summary>
    public bool NoAnsiColor { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.UseFFmpegConcatDemuxer"/>.
    /// </summary>
    public bool UseFFmpegConcatDemuxer { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DelAfterDone"/>.
    /// </summary>
    public bool DelAfterDone { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.AutoSubtitleFix"/>.
    /// </summary>
    public bool AutoSubtitleFix { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.CheckSegmentsCount"/>.
    /// </summary>
    public bool CheckSegmentsCount { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SkipDownload"/>.
    /// </summary>
    public bool SkipDownload { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.WriteMetaJson"/>.
    /// </summary>
    public bool WriteMetaJson { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.AppendUrlParams"/>.
    /// </summary>
    public bool AppendUrlParams { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.MP4RealTimeDecryption"/>.
    /// </summary>
    public bool MP4RealTimeDecryption { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.UseShakaPackager"/>.
    /// </summary>
    [Obsolete("Use DecryptionEngine instead")]
    public bool UseShakaPackager { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DecryptionEngine"/>.
    /// </summary>
    public DecryptEngine DecryptionEngine { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.MuxAfterDone"/>.
    /// </summary>
    public bool MuxAfterDone { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.ConcurrentDownload"/>.
    /// </summary>
    public bool ConcurrentDownload { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LiveRealTimeMerge"/>.
    /// </summary>
    public bool LiveRealTimeMerge { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LiveKeepSegments"/>.
    /// </summary>
    public bool LiveKeepSegments { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LivePerformAsVod"/>.
    /// </summary>
    public bool LivePerformAsVod { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.UseSystemProxy"/>.
    /// </summary>
    public bool UseSystemProxy { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SubtitleFormat"/>.
    /// </summary>
    public SubtitleFormat SubtitleFormat { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.TmpDir"/>.
    /// </summary>
    public string? TmpDir { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SaveDir"/>.
    /// </summary>
    public string? SaveDir { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SaveName"/>.
    /// </summary>
    public string? SaveName { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SavePattern"/>.
    /// </summary>
    public string? SavePattern { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LogFilePath"/>.
    /// </summary>
    public string? LogFilePath { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.UILanguage"/>.
    /// </summary>
    public string? UILanguage { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>.
    /// </summary>
    public string? DecryptionBinaryPath { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.FFmpegBinaryPath"/>.
    /// </summary>
    public string? FFmpegBinaryPath { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.MkvmergeBinaryPath"/>.
    /// </summary>
    public string? MkvmergeBinaryPath { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.MuxImports"/>.
    /// </summary>
    public List<OutputFile>? MuxImports { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.VideoFilter"/>.
    /// </summary>
    public StreamFilter? VideoFilter { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DropVideoFilter"/>.
    /// </summary>
    public StreamFilter? DropVideoFilter { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.AudioFilter"/>.
    /// </summary>
    public StreamFilter? AudioFilter { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DropAudioFilter"/>.
    /// </summary>
    public StreamFilter? DropAudioFilter { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.SubtitleFilter"/>.
    /// </summary>
    public StreamFilter? SubtitleFilter { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.DropSubtitleFilter"/>.
    /// </summary>
    public StreamFilter? DropSubtitleFilter { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.CustomHLSMethod"/>.
    /// </summary>
    public EncryptMethod? CustomHLSMethod { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.CustomHLSKey"/>.
    /// </summary>
    public byte[]? CustomHLSKey { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.CustomHLSIv"/>.
    /// </summary>
    public byte[]? CustomHLSIv { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.CustomProxy"/>.
    /// </summary>
    public WebProxy? CustomProxy { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.CustomRange"/>.
    /// </summary>
    public CustomRange? CustomRange { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LiveWaitTime"/>.
    /// </summary>
    public int? LiveWaitTime { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LiveTakeCount"/>.
    /// </summary>
    public int LiveTakeCount { get; set; }
    public MuxOptions? MuxOptions { get; set; }
    // public bool LiveWriteHLS { get; set; } = true;
    /// <summary>
    /// See: <see cref="CommandInvoker.LivePipeMux"/>.
    /// </summary>
    public bool LivePipeMux { get; set; }
    /// <summary>
    /// See: <see cref="CommandInvoker.LiveFixVttByAudio"/>.
    /// </summary>
    public bool LiveFixVttByAudio { get; set; }
}


================================================
FILE: src/N_m3u8DL-RE/Config/DownloaderConfig.cs
================================================
using N_m3u8DL_RE.CommandLine;

namespace N_m3u8DL_RE.Config;

internal class DownloaderConfig
{
    public required MyOption MyOptions { get; set; }

    /// <summary>
    /// 前置阶段生成的文件夹名
    /// </summary>
    public required string DirPrefix { get; set; }
    /// <summary>
    /// 文件名模板
    /// </summary>
    public string? SavePattern { get; set; }
    /// <summary>
    /// 校验响应头的文件大小和实际大小
    /// </summary>
    public bool CheckContentLength { get; set; } = true;
    /// <summary>
    /// 请求头
    /// </summary>
    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
}

================================================
FILE: src/N_m3u8DL-RE/Config/EnvConfigKey.cs
================================================
namespace N_m3u8DL_RE.Config;

/// <summary>
/// 通过配置环境变量来实现更细节地控制某些逻辑
/// </summary>
public static class EnvConfigKey
{
    /// <summary>
    /// 当此值为1时, 在图形字幕处理逻辑中PNG生成后不再删除m4s文件
    /// </summary>
    public const string ReKeepImageSegments = "RE_KEEP_IMAGE_SEGMENTS";
    
    /// <summary>
    /// 控制启用PipeMux时, 具体ffmpeg命令行
    /// </summary>
    public const string ReLivePipeOptions = "RE_LIVE_PIPE_OPTIONS";
    
    /// <summary>
    /// 控制启用PipeMux时, 非Windows环境下命名管道文件的生成目录
    /// </summary>
    public const string ReLivePipeTmpDir = "RE_LIVE_PIPE_TMP_DIR";
}

================================================
FILE: src/N_m3u8DL-RE/Crypto/AESUtil.cs
================================================
using System.Security.Cryptography;

namespace N_m3u8DL_RE.Crypto;

internal static class AESUtil
{
    /// <summary>
    /// AES-128解密,解密后原地替换文件
    /// </summary>
    /// <param name="filePath"></param>
    /// <param name="keyByte"></param>
    /// <param name="ivByte"></param>
    /// <param name="mode"></param>
    /// <param name="padding"></param>
    public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
    {
        var fileBytes = File.ReadAllBytes(filePath);
        var decrypted = AES128Decrypt(fileBytes, keyByte, ivByte, mode, padding);
        File.WriteAllBytes(filePath, decrypted);
    }

    public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
    {
        byte[] inBuff = encryptedBuff;

        Aes dcpt = Aes.Create();
        dcpt.BlockSize = 128;
        dcpt.KeySize = 128;
        dcpt.Key = keyByte;
        dcpt.IV = ivByte;
        dcpt.Mode = mode;
        dcpt.Padding = padding;

        ICryptoTransform cTransform = dcpt.CreateDecryptor();
        byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length);
        return resultArray;
    }
}

================================================
FILE: src/N_m3u8DL-RE/Crypto/CSChaCha20.cs
================================================
/*
 * Copyright (c) 2015, 2018 Scott Bennett
 *           (c) 2018-2021 Kaarlo Räihä
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.CompilerServices; // For MethodImplOptions.AggressiveInlining

namespace CSChaCha20
{
    /// <summary>
    /// Class that can be used for ChaCha20 encryption / decryption
    /// </summary>
    public sealed class ChaCha20 : IDisposable
    {
        /// <summary>
        /// Only allowed key lenght in bytes
        /// </summary>
        public const int allowedKeyLength = 32;

        /// <summary>
        /// Only allowed nonce lenght in bytes
        /// </summary>
        public const int allowedNonceLength = 12;

        /// <summary>
        /// How many bytes are processed per loop
        /// </summary>
        public const int processBytesAtTime = 64;

        private const int stateLength = 16;

        /// <summary>
        /// The ChaCha20 state (aka "context")
        /// </summary>
        private readonly uint[] state = new uint[stateLength];

        /// <summary>
        /// Determines if the objects in this class have been disposed of. Set to true by the Dispose() method.
        /// </summary>
        private bool isDisposed = false;

        /// <summary>
        /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens.
        /// </summary>
        /// <remarks>
        /// See <a href="https://tools.ietf.org/html/rfc7539#page-10">ChaCha20 Spec Section 2.4</a> for a detailed description of the inputs.
        /// </remarks>
        /// <param name="key">
        /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers
        /// </param>
        /// <param name="nonce">
        /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers
        /// </param>
        /// <param name="counter">
        /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
        /// </param>
        public ChaCha20(byte[] key, byte[] nonce, uint counter)
        {
            this.KeySetup(key);
            this.IvSetup(nonce, counter);
        }

#if NET6_0_OR_GREATER

		/// <summary>
		/// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens.
		/// </summary>
		/// <remarks>
		/// See <a href="https://tools.ietf.org/html/rfc7539#page-10">ChaCha20 Spec Section 2.4</a> for a detailed description of the inputs.
		/// </remarks>
		/// <param name="key">A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers</param>
		/// <param name="nonce">A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers</param>
		/// <param name="counter">A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer</param>
		public ChaCha20(ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce, uint counter) 
		{
			this.KeySetup(key.ToArray());
			this.IvSetup(nonce.ToArray(), counter);
		}

#endif // NET6_0_OR_GREATER

        /// <summary>
        /// The ChaCha20 state (aka "context"). Read-Only.
        /// </summary>
        public uint[] State
        {
            get
            {
                return this.state;
            }
        }


        // These are the same constants defined in the reference implementation.
        // http://cr.yp.to/streamciphers/timings/estreambench/submissions/salsa20/chacha8/ref/chacha.c
        private static readonly byte[] sigma = Encoding.ASCII.GetBytes("expand 32-byte k");
        private static readonly byte[] tau = Encoding.ASCII.GetBytes("expand 16-byte k");

        /// <summary>
        /// Set up the ChaCha state with the given key. A 32-byte key is required and enforced.
        /// </summary>
        /// <param name="key">
        /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers
        /// </param>
        private void KeySetup(byte[] key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("Key is null");
            }

            if (key.Length != allowedKeyLength)
            {
                throw new ArgumentException($"Key length must be {allowedKeyLength}. Actual: {key.Length}");
            }

            state[4] = Util.U8To32Little(key, 0);
            state[5] = Util.U8To32Little(key, 4);
            state[6] = Util.U8To32Little(key, 8);
            state[7] = Util.U8To32Little(key, 12);

            byte[] constants = (key.Length == allowedKeyLength) ? sigma : tau;
            int keyIndex = key.Length - 16;

            state[8] = Util.U8To32Little(key, keyIndex + 0);
            state[9] = Util.U8To32Little(key, keyIndex + 4);
            state[10] = Util.U8To32Little(key, keyIndex + 8);
            state[11] = Util.U8To32Little(key, keyIndex + 12);

            state[0] = Util.U8To32Little(constants, 0);
            state[1] = Util.U8To32Little(constants, 4);
            state[2] = Util.U8To32Little(constants, 8);
            state[3] = Util.U8To32Little(constants, 12);
        }

        /// <summary>
        /// Set up the ChaCha state with the given nonce (aka Initialization Vector or IV) and block counter. A 12-byte nonce and a 4-byte counter are required.
        /// </summary>
        /// <param name="nonce">
        /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers
        /// </param>
        /// <param name="counter">
        /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer
        /// </param>
        private void IvSetup(byte[] nonce, uint counter)
        {
            if (nonce == null)
            {
                // There has already been some state set up. Clear it before exiting.
                Dispose();
                throw new ArgumentNullException("Nonce is null");
            }

            if (nonce.Length != allowedNonceLength)
            {
                // There has already been some state set up. Clear it before exiting.
                Dispose();
                throw new ArgumentException($"Nonce length must be {allowedNonceLength}. Actual: {nonce.Length}");
            }

            state[12] = counter;
            state[13] = Util.U8To32Little(nonce, 0);
            state[14] = Util.U8To32Little(nonce, 4);
            state[15] = Util.U8To32Little(nonce, 8);
        }


        #region Encryption methods

        /// <summary>
        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="output">Output byte array, must have enough bytes</param>
        /// <param name="input">Input byte array</param>
        /// <param name="numBytes">Number of bytes to encrypt</param>
        public void EncryptBytes(byte[] output, byte[] input, int numBytes)
        {
            this.WorkBytes(output, input, numBytes);
        }

        /// <summary>
        /// Encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
        /// </summary>
        /// <param name="output">Output stream</param>
        /// <param name="input">Input stream</param>
        /// <param name="howManyBytesToProcessAtTime">How many bytes to read and write at time, default is 1024</param>
        public void EncryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)
        {
            this.WorkStreams(output, input, howManyBytesToProcessAtTime);
        }

        /// <summary>
        /// Async encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
        /// </summary>
        /// <param name="output">Output stream</param>
        /// <param name="input">Input stream</param>
        /// <param name="howManyBytesToProcessAtTime">How many bytes to read and write at time, default is 1024</param>
        public async Task EncryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)
        {
            await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime);
        }

        /// <summary>
        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="output">Output byte array, must have enough bytes</param>
        /// <param name="input">Input byte array</param>
        public void EncryptBytes(byte[] output, byte[] input)
        {
            this.WorkBytes(output, input, input.Length);
        }

        /// <summary>
        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="input">Input byte array</param>
        /// <param name="numBytes">Number of bytes to encrypt</param>
        /// <returns>Byte array that contains encrypted bytes</returns>
        public byte[] EncryptBytes(byte[] input, int numBytes)
        {
            byte[] returnArray = new byte[numBytes];
            this.WorkBytes(returnArray, input, numBytes);
            return returnArray;
        }

        /// <summary>
        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="input">Input byte array</param>
        /// <returns>Byte array that contains encrypted bytes</returns>
        public byte[] EncryptBytes(byte[] input)
        {
            byte[] returnArray = new byte[input.Length];
            this.WorkBytes(returnArray, input, input.Length);
            return returnArray;
        }

        /// <summary>
        /// Encrypt string as UTF8 byte array, returns byte array that is allocated by method.
        /// </summary>
        /// <remarks>Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform</remarks>
        /// <param name="input">Input string</param>
        /// <returns>Byte array that contains encrypted bytes</returns>
        public byte[] EncryptString(string input)
        {
            byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(input);
            byte[] returnArray = new byte[utf8Bytes.Length];

            this.WorkBytes(returnArray, utf8Bytes, utf8Bytes.Length);
            return returnArray;
        }

        #endregion // Encryption methods


        #region // Decryption methods

        /// <summary>
        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to the output buffer.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="output">Output byte array</param>
        /// <param name="input">Input byte array</param>
        /// <param name="numBytes">Number of bytes to decrypt</param>
        public void DecryptBytes(byte[] output, byte[] input, int numBytes)
        {
            this.WorkBytes(output, input, numBytes);
        }

        /// <summary>
        /// Decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
        /// </summary>
        /// <param name="output">Output stream</param>
        /// <param name="input">Input stream</param>
        /// <param name="howManyBytesToProcessAtTime">How many bytes to read and write at time, default is 1024</param>
        public void DecryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)
        {
            this.WorkStreams(output, input, howManyBytesToProcessAtTime);
        }

        /// <summary>
        /// Async decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)
        /// </summary>
        /// <param name="output">Output stream</param>
        /// <param name="input">Input stream</param>
        /// <param name="howManyBytesToProcessAtTime">How many bytes to read and write at time, default is 1024</param>
        public async Task DecryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)
        {
            await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime);
        }

        /// <summary>
        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="output">Output byte array, must have enough bytes</param>
        /// <param name="input">Input byte array</param>
        public void DecryptBytes(byte[] output, byte[] input)
        {
            WorkBytes(output, input, input.Length);
        }

        /// <summary>
        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="input">Input byte array</param>
        /// <param name="numBytes">Number of bytes to encrypt</param>
        /// <returns>Byte array that contains decrypted bytes</returns>
        public byte[] DecryptBytes(byte[] input, int numBytes)
        {
            byte[] returnArray = new byte[numBytes];
            WorkBytes(returnArray, input, numBytes);
            return returnArray;
        }

        /// <summary>
        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.
        /// </summary>
        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>
        /// <param name="input">Input byte array</param>
        /// <returns>Byte array that contains decrypted bytes</returns>
        public byte[] DecryptBytes(byte[] input)
        {
            byte[] returnArray = new byte[input.Length];
            WorkBytes(returnArray, input, input.Length);
            return returnArray;
        }

        /// <summary>
        /// Decrypt UTF8 byte array to string.
        /// </summary>
        /// <remarks>Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform</remarks>
        /// <param name="input">Byte array</param>
        /// <returns>Byte array that contains encrypted bytes</returns>
        public string DecryptUTF8ByteArray(byte[] input)
        {
            byte[] tempArray = new byte[input.Length];

            WorkBytes(tempArray, input, input.Length);
            return System.Text.Encoding.UTF8.GetString(tempArray);
        }

        #endregion // Decryption methods

        private void WorkStreams(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)
        {
            int readBytes;

            byte[] inputBuffer = new byte[howManyBytesToProcessAtTime];
            byte[] outputBuffer = new byte[howManyBytesToProcessAtTime];

            while ((readBytes = input.Read(inputBuffer, 0, howManyBytesToProcessAtTime)) > 0)
            {
                // Encrypt or decrypt
                WorkBytes(output: outputBuffer, input: inputBuffer, numBytes: readBytes);

                // Write buffer
                output.Write(outputBuffer, 0, readBytes);
            }
        }

        private async Task WorkStreamsAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)
        {
            byte[] readBytesBuffer = new byte[howManyBytesToProcessAtTime];
            byte[] writeBytesBuffer = new byte[howManyBytesToProcessAtTime];
            int howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime);

            while (howManyBytesWereRead > 0)
            {
                // Encrypt or decrypt
                WorkBytes(output: writeBytesBuffer, input: readBytesBuffer, numBytes: howManyBytesWereRead);

                // Write
                await output.WriteAsync(writeBytesBuffer, 0, howManyBytesWereRead);

                // Read more
                howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime);
            }
        }

        /// <summary>
        /// Encrypt or decrypt an arbitrary-length byte array (input), writing the resulting byte array to the output buffer. The number of bytes to read from the input buffer is determined by numBytes.
        /// </summary>
        /// <param name="output">Output byte array</param>
        /// <param name="input">Input byte array</param>
        /// <param name="numBytes">How many bytes to process</param>
        private void WorkBytes(byte[] output, byte[] input, int numBytes)
        {
            if (isDisposed)
            {
                throw new ObjectDisposedException("state", "The ChaCha state has been disposed");
            }

            if (input == null)
            {
                throw new ArgumentNullException("input", "Input cannot be null");
            }

            if (output == null)
            {
                throw new ArgumentNullException("output", "Output cannot be null");
            }

            if (numBytes < 0 || numBytes > input.Length)
            {
                throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]");
            }

            if (output.Length < numBytes)
            {
                throw new ArgumentOutOfRangeException("output", $"Output byte array should be able to take at least {numBytes}");
            }

            uint[] x = new uint[stateLength];    // Working buffer
            byte[] tmp = new byte[processBytesAtTime];  // Temporary buffer
            int offset = 0;

            while (numBytes > 0)
            {
                // Copy state to working buffer
                Buffer.BlockCopy(this.state, 0, x, 0, stateLength * sizeof(uint));

                for (int i = 0; i < 10; i++)
                {
                    QuarterRound(x, 0, 4, 8, 12);
                    QuarterRound(x, 1, 5, 9, 13);
                    QuarterRound(x, 2, 6, 10, 14);
                    QuarterRound(x, 3, 7, 11, 15);

                    QuarterRound(x, 0, 5, 10, 15);
                    QuarterRound(x, 1, 6, 11, 12);
                    QuarterRound(x, 2, 7, 8, 13);
                    QuarterRound(x, 3, 4, 9, 14);
                }

                for (int i = 0; i < stateLength; i++)
                {
                    Util.ToBytes(tmp, Util.Add(x[i], this.state[i]), 4 * i);
                }

                this.state[12] = Util.AddOne(state[12]);
                if (this.state[12] <= 0)
                {
                    /* Stopping at 2^70 bytes per nonce is the user's responsibility */
                    this.state[13] = Util.AddOne(state[13]);
                }

                // In case these are last bytes
                if (numBytes <= processBytesAtTime)
                {
                    for (int i = 0; i < numBytes; i++)
                    {
                        output[i + offset] = (byte)(input[i + offset] ^ tmp[i]);
                    }

                    return;
                }

                for (int i = 0; i < processBytesAtTime; i++)
                {
                    output[i + offset] = (byte)(input[i + offset] ^ tmp[i]);
                }

                numBytes -= processBytesAtTime;
                offset += processBytesAtTime;
            }
        }

        /// <summary>
        /// The ChaCha Quarter Round operation. It operates on four 32-bit unsigned integers within the given buffer at indices a, b, c, and d.
        /// </summary>
        /// <remarks>
        /// The ChaCha state does not have four integer numbers: it has 16. So the quarter-round operation works on only four of them -- hence the name. Each quarter round operates on four predetermined numbers in the ChaCha state.
        /// See <a href="https://tools.ietf.org/html/rfc7539#page-4">ChaCha20 Spec Sections 2.1 - 2.2</a>.
        /// </remarks>
        /// <param name="x">A ChaCha state (vector). Must contain 16 elements.</param>
        /// <param name="a">Index of the first number</param>
        /// <param name="b">Index of the second number</param>
        /// <param name="c">Index of the third number</param>
        /// <param name="d">Index of the fourth number</param>
        private static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d)
        {
            x[a] = Util.Add(x[a], x[b]);
            x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 16);

            x[c] = Util.Add(x[c], x[d]);
            x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 12);

            x[a] = Util.Add(x[a], x[b]);
            x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 8);

            x[c] = Util.Add(x[c], x[d]);
            x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 7);
        }

        #region Destructor and Disposer

        /// <summary>
        /// Clear and dispose of the internal state. The finalizer is only called if Dispose() was never called on this cipher.
        /// </summary>
        ~ChaCha20()
        {
            Dispose(false);
        }

        /// <summary>
        /// Clear and dispose of the internal state. Also request the GC not to call the finalizer, because all cleanup has been taken care of.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            /*
			 * The Garbage Collector does not need to invoke the finalizer because Dispose(bool) has already done all the cleanup needed.
			 */
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// This method should only be invoked from Dispose() or the finalizer. This handles the actual cleanup of the resources.
        /// </summary>
        /// <param name="disposing">
        /// Should be true if called by Dispose(); false if called by the finalizer
        /// </param>
        private void Dispose(bool disposing)
        {
            if (!isDisposed)
            {
                if (disposing)
                {
                    /* Cleanup managed objects by calling their Dispose() methods */
                }

                /* Cleanup any unmanaged objects here */
                Array.Clear(state, 0, stateLength);
            }

            isDisposed = true;
        }

        #endregion // Destructor and Disposer
    }

    /// <summary>
    /// Utilities that are used during compression
    /// </summary>
    public static class Util
    {
        /// <summary>
        /// n-bit left rotation operation (towards the high bits) for 32-bit integers.
        /// </summary>
        /// <param name="v"></param>
        /// <param name="c"></param>
        /// <returns>The result of (v LEFTSHIFT c)</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static uint Rotate(uint v, int c)
        {
            unchecked
            {
                return (v << c) | (v >> (32 - c));
            }
        }

        /// <summary>
        /// Unchecked integer exclusive or (XOR) operation.
        /// </summary>
        /// <param name="v"></param>
        /// <param name="w"></param>
        /// <returns>The result of (v XOR w)</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static uint XOr(uint v, uint w)
        {
            return unchecked(v ^ w);
        }

        /// <summary>
        /// Unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32.
        /// </summary>
        /// <remarks>
        /// See <a href="https://tools.ietf.org/html/rfc7539#page-4">ChaCha20 Spec Section 2.1</a>.
        /// </remarks>
        /// <param name="v"></param>
        /// <param name="w"></param>
        /// <returns>The result of (v + w) modulo 2^32</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static uint Add(uint v, uint w)
        {
            return unchecked(v + w);
        }

        /// <summary>
        /// Add 1 to the input parameter using unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32.
        /// </summary>
        /// <remarks>
        /// See <a href="https://tools.ietf.org/html/rfc7539#page-4">ChaCha20 Spec Section 2.1</a>.
        /// </remarks>
        /// <param name="v"></param>
        /// <returns>The result of (v + 1) modulo 2^32</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static uint AddOne(uint v)
        {
            return unchecked(v + 1);
        }

        /// <summary>
        /// Convert four bytes of the input buffer into an unsigned 32-bit integer, beginning at the inputOffset.
        /// </summary>
        /// <param name="p"></param>
        /// <param name="inputOffset"></param>
        /// <returns>An unsigned 32-bit integer</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static uint U8To32Little(byte[] p, int inputOffset)
        {
            unchecked
            {
                return ((uint)p[inputOffset]
                    | ((uint)p[inputOffset + 1] << 8)
                    | ((uint)p[inputOffset + 2] << 16)
                    | ((uint)p[inputOffset + 3] << 24));
            }
        }

        /// <summary>
        /// Serialize the input integer into the output buffer. The input integer will be split into 4 bytes and put into four sequential places in the output buffer, starting at the outputOffset.
        /// </summary>
        /// <param name="output"></param>
        /// <param name="input"></param>
        /// <param name="outputOffset"></param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void ToBytes(byte[] output, uint input, int outputOffset)
        {
            unchecked
            {
                output[outputOffset] = (byte)input;
                output[outputOffset + 1] = (byte)(input >> 8);
                output[outputOffset + 2] = (byte)(input >> 16);
                output[outputOffset + 3] = (byte)(input >> 24);
            }
        }
    }
}

================================================
FILE: src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs
================================================
using CSChaCha20;

namespace N_m3u8DL_RE.Crypto;

internal static class ChaCha20Util
{
    public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes)
    {
        if (keyBytes.Length != 32)
            throw new Exception("Key must be 32 bytes!");
        if (nonceBytes.Length != 12 && nonceBytes.Length != 8)
            throw new Exception("Key must be 12 or 8 bytes!");
        if (nonceBytes.Length == 8)
            nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray();

        var decStream = new MemoryStream();
        using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff));
        using (BinaryWriter writer = new BinaryWriter(decStream))
            while (true)
            {
                var buffer = reader.ReadBytes(1024);
                byte[] dec = new byte[buffer.Length];
                if (buffer.Length > 0)
                {
                    ChaCha20 forDecrypting = new ChaCha20(keyBytes, nonceBytes, 0);
                    forDecrypting.DecryptBytes(dec, buffer);
                    writer.Write(dec, 0, dec.Length);
                }
                else
                {
                    break;
                }
            }

        return decStream.ToArray();
    }
}

================================================
FILE: src/N_m3u8DL-RE/Directory.Build.props
================================================
<Project>

    <PropertyGroup>
      <IlcOptimizationPreference>Speed</IlcOptimizationPreference>
      <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
      <StaticallyLinked Condition="$(RuntimeIdentifier.StartsWith('win'))">true</StaticallyLinked>
      <TrimMode>full</TrimMode>
      <TrimmerDefaultAction>link</TrimmerDefaultAction>
      <IlcTrimMetadata>true</IlcTrimMetadata>
      <IlcGenerateStackTraceData>true</IlcGenerateStackTraceData>
      <SatelliteResourceLanguages>zh-CN;zh-TW;en-US</SatelliteResourceLanguages>
      <PublishAot>true</PublishAot>
      <StripSymbols>true</StripSymbols>
      <ObjCopyName Condition="'$(RuntimeIdentifier)' == 'linux-arm64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">aarch64-linux-gnu-objcopy</ObjCopyName>
    </PropertyGroup>

  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-musl-arm64' or '$(RuntimeIdentifier)' == 'linux-musl-x64'">
    <!-- 导入libc -->
    <DirectPInvoke Include="libc" />
  </ItemGroup>

  <PropertyGroup Condition="'$(RuntimeIdentifier)' == 'linux-musl-arm64' or '$(RuntimeIdentifier)' == 'linux-musl-x64'">
    <!-- 设置为纯静态应用 -->
    <StaticExecutable>true</StaticExecutable>
    <!-- 静态链接openssl -->
    <StaticOpenSslLinking>true</StaticOpenSslLinking>
    <!-- 去除国际化支持 -->
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>

    <!--<ItemGroup Condition="'$(PublishAot)' == 'true' and '$(RuntimeIdentifier)' != 'win-arm64' and '$(RuntimeIdentifier)' != 'linux-arm64' and '$(RuntimeIdentifier)' != 'osx-arm64' and '$(RuntimeIdentifier)' != 'osx-x64'">
        <PackageReference Include="PublishAotCompressed" Version="1.0.3" />
    </ItemGroup>-->

</Project>


================================================
FILE: src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs
================================================
using N_m3u8DL_RE.Column;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Config;
using N_m3u8DL_RE.Downloader;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Parser;
using N_m3u8DL_RE.Util;
using Spectre.Console;
using System.Collections.Concurrent;
using System.Text;

namespace N_m3u8DL_RE.DownloadManager;

internal class HTTPLiveRecordManager
{
    private static HttpClient HttpClient = new();
    
    IDownloader Downloader;
    DownloaderConfig DownloaderConfig;
    StreamExtractor StreamExtractor;
    List<StreamSpec> SelectedSteams;
    List<OutputFile> OutputFiles = [];
    DateTime NowDateTime;
    DateTime? PublishDateTime;
    bool STOP_FLAG = false;
    bool READ_IFO = false;
    ConcurrentDictionary<int, int> RecordingDurDic = new(); // 已录制时长
    ConcurrentDictionary<int, double> RecordingSizeDic = new(); // 已录制大小
    CancellationTokenSource CancellationTokenSource = new(); // 取消Wait
    List<byte> InfoBuffer = new List<byte>(188 * 5000); // 5000个分包中解析信息,没有就算了

    public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
    {
        this.DownloaderConfig = downloaderConfig;
        Downloader = new SimpleDownloader(DownloaderConfig);
        NowDateTime = DateTime.Now;
        PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;
        StreamExtractor = streamExtractor;
        SelectedSteams = selectedSteams;
    }

    private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)
    {
        task.MaxValue = 1;
        task.StartTask();

        var name = streamSpec.ToShortString();
        var dirName = $"{DownloaderConfig.MyOptions.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}";
        var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;

        // Use SavePattern if provided, otherwise use SaveName or dirName
        var saveName = dirName;
        if (!string.IsNullOrWhiteSpace(DownloaderConfig.MyOptions.SavePattern))
        {
            saveName = OtherUtil.FormatSavePattern(DownloaderConfig.MyOptions.SavePattern, streamSpec, DownloaderConfig.MyOptions.SaveName, task.Id);
        }
        else if (DownloaderConfig.MyOptions.SaveName != null)
        {
            saveName = $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.');
        }

        Logger.Debug($"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}");

        // 创建文件夹
        if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);

        using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url));
        request.Headers.ConnectionClose = false;
        foreach (var item in DownloaderConfig.Headers)
        {
            request.Headers.TryAddWithoutValidation(item.Key, item.Value);
        }
        Logger.Debug(request.Headers.ToString());

        using var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationTokenSource.Token);
        response.EnsureSuccessStatusCode();

        var output = Path.Combine(saveDir, saveName + ".ts");
        using var stream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
        using var responseStream = await response.Content.ReadAsStreamAsync(CancellationTokenSource.Token);
        var buffer = new byte[16 * 1024];
        var size = 0;

        // 计时器
        _ = TimeCounterAsync();
        // 读取INFO
        _ = ReadInfoAsync();

        try
        {
            while ((size = await responseStream.ReadAsync(buffer, CancellationTokenSource.Token)) > 0)
            {
                if (!READ_IFO && InfoBuffer.Count < 188 * 5000)
                {
                    InfoBuffer.AddRange(buffer);
                }
                speedContainer.Add(size);
                RecordingSizeDic[task.Id] += size;
                await stream.WriteAsync(buffer, 0, size);
            }
        }
        catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
        {
            ;
        }

        Logger.InfoMarkUp("File Size: " + GlobalUtil.FormatFileSize(RecordingSizeDic[task.Id]));

        return true;
    }

    public async Task ReadInfoAsync()
    {
        while (!STOP_FLAG && !READ_IFO)
        {
            await Task.Delay(200);
            if (InfoBuffer.Count < 188 * 5000) continue;

            ushort ConvertToUint16(IEnumerable<byte> bytes)
            {
                if (BitConverter.IsLittleEndian)
                    bytes = bytes.Reverse();
                return BitConverter.ToUInt16(bytes.ToArray());
            }

            var data = InfoBuffer.ToArray();
            var programId = "";
            var serviceProvider = "";
            var serviceName = "";
            for (int i = 0; i < data.Length; i++)
            {
                if (data[i] == 0x47 && (i + 188) < data.Length && data[i + 188] == 0x47)
                {
                    var tsData = data.Skip(i).Take(188);
                    var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0);
                    var pid = (tsHeaderInt & 0x1fff00) >> 8;
                    var tsPayload = tsData.Skip(4);
                    // PAT
                    if (pid == 0x0000)
                    {
                        programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString();
                    }
                    // SDT, BAT, ST
                    else if (pid == 0x0011)
                    {
                        var tableId = (int)tsPayload.Skip(1).First();
                        // Current TS Info
                        if (tableId == 0x42)
                        {
                            var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff;
                            var sectionData = tsPayload.Skip(4).Take(sectionLength);
                            var dscripData = sectionData.Skip(8);
                            var descriptorsLoopLength = (ConvertToUint16(dscripData.Skip(3).Take(2))) & 0xfff;
                            var descriptorsData = dscripData.Skip(5).Take(descriptorsLoopLength);
                            var serviceProviderLength = (int)descriptorsData.Skip(3).First();
                            serviceProvider = Encoding.UTF8.GetString(descriptorsData.Skip(4).Take(serviceProviderLength).ToArray());
                            var serviceNameLength = (int)descriptorsData.Skip(4 + serviceProviderLength).First();
                            serviceName = Encoding.UTF8.GetString(descriptorsData.Skip(5 + serviceProviderLength).Take(serviceNameLength).ToArray());
                        }
                    }
                    if (programId != "" && (serviceName != "" || serviceProvider != ""))
                        break;
                }
            }

            if (!string.IsNullOrEmpty(programId))
            {
                Logger.InfoMarkUp($"Program Id: [cyan]{programId.EscapeMarkup()}[/]");
                if (!string.IsNullOrEmpty(serviceName)) Logger.InfoMarkUp($"Service Name: [cyan]{serviceName.EscapeMarkup()}[/]");
                if (!string.IsNullOrEmpty(serviceProvider)) Logger.InfoMarkUp($"Service Provider: [cyan]{serviceProvider.EscapeMarkup()}[/]");
                READ_IFO = true;
            }
        }
    }

    public async Task TimeCounterAsync()
    {
        while (!STOP_FLAG)
        {
            await Task.Delay(1000);
            RecordingDurDic[0]++;

            // 检测时长限制
            if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds))
            {
                Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
                STOP_FLAG = true;
                CancellationTokenSource.Cancel();
            }
        }
    }

    public async Task<bool> StartRecordAsync()
    {
        ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算
        ConcurrentDictionary<StreamSpec, bool?> Results = new();

        var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
        progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;

        // 进度条的列定义
        var progressColumns = new ProgressColumn[]
        {
            new TaskDescriptionColumn() { Alignment = Justify.Left },
            new RecordingDurationColumn(RecordingDurDic), // 时长显示
            new RecordingSizeColumn(RecordingSizeDic), // 大小显示
            new RecordingStatusColumn(),
            new DownloadSpeedColumn(SpeedContainerDic), // 速度计算
            new SpinnerColumn(),
        };
        if (DownloaderConfig.MyOptions.NoAnsiColor)
        {
            progressColumns = progressColumns.SkipLast(1).ToArray();
        }
        progress.Columns(progressColumns);

        await progress.StartAsync(async ctx =>
        {
            // 创建任务
            var dic = SelectedSteams.Select(item =>
            {
                var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0);
                SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算
                RecordingDurDic[task.Id] = 0;
                RecordingSizeDic[task.Id] = 0;
                return (item, task);
            }).ToDictionary(item => item.item, item => item.task);

            DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;
            var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
            if (limit != TimeSpan.MaxValue)
                Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");
            // 录制直播时,用户选了几个流就并发录几个
            var options = new ParallelOptions()
            {
                MaxDegreeOfParallelism = SelectedSteams.Count
            };
            // 并发下载
            await Parallel.ForEachAsync(dic, options, async (kp, _) =>
            {
                var task = kp.Value;
                var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);
                Results[kp.Key] = await consumerTask;
            });
        });

        var success = Results.Values.All(v => v == true);

        return success;
    }
}

================================================
FILE: src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs
================================================
using Mp4SubtitleParser;
using N_m3u8DL_RE.Column;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Config;
using N_m3u8DL_RE.Downloader;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Parser;
using N_m3u8DL_RE.Parser.Mp4;
using N_m3u8DL_RE.Util;
using Spectre.Console;
using System.Collections.Concurrent;
using System.Text;
using N_m3u8DL_RE.Enum;

namespace N_m3u8DL_RE.DownloadManager;

internal class SimpleDownloadManager
{
    IDownloader Downloader;
    DownloaderConfig DownloaderConfig;
    StreamExtractor StreamExtractor;
    List<StreamSpec> SelectedSteams;
    List<OutputFile> OutputFiles = [];

    public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) 
    { 
        this.DownloaderConfig = downloaderConfig;
        this.SelectedSteams = selectedSteams;
        this.StreamExtractor = streamExtractor;
        Downloader = new SimpleDownloader(DownloaderConfig);
    }

    // 从文件读取KEY
    private async Task SearchKeyAsync(string? currentKID)
    {
        var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID);
        if (_key != null)
        {
            if (DownloaderConfig.MyOptions.Keys == null)
                DownloaderConfig.MyOptions.Keys = [_key];
            else
                DownloaderConfig.MyOptions.Keys = [..DownloaderConfig.MyOptions.Keys, _key];
        }
    }

    private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)
    {
        if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))
        {
            DownloaderConfig.MyOptions.BinaryMerge = true;
            Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]");
        }

        if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))
        {
            DownloaderConfig.MyOptions.MuxAfterDone = false;
            Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
        }

        if (mediainfos.Where(m => m.Type == "Audio").All(m => m.BaseInfo!.Contains("aac")))
        {
            useAACFilter = true;
        }

        if (mediainfos.All(m => m.Type == "Audio"))
        {
            streamSpec.MediaType = MediaType.AUDIO;
        }
        else if (mediainfos.All(m => m.Type == "Subtitle"))
        {
            streamSpec.MediaType = MediaType.SUBTITLES;
            if (streamSpec.Extension is null or "ts")
                streamSpec.Extension = "vtt";
        }
    }

    private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)
    {
        speedContainer.ResetVars();
        bool useAACFilter = false; // ffmpeg合并flag
        List<Mediainfo> mediaInfos = [];
        ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();

        var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments);
        if (segments == null || !segments.Any()) return false;
        // 单分段尝试切片并行下载
        if (segments.Count() == 1)
        {
            var splitSegments = await LargeSingleFileSplitUtil.SplitUrlAsync(segments.First(), DownloaderConfig.Headers);
            if (splitSegments != null)
            {
                segments = splitSegments;
                Logger.WarnMarkUp($"[darkorange3_1]{ResString.singleFileSplitWarn}[/]");
                if (DownloaderConfig.MyOptions.MP4RealTimeDecryption)
                {
                    DownloaderConfig.MyOptions.MP4RealTimeDecryption = false;
                    Logger.WarnMarkUp($"[darkorange3_1]{ResString.singleFileRealtimeDecryptWarn}[/]");
                }
            }
            else speedContainer.SingleSegment = true;
        }

        var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO;
        var dirName = $"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}";
        var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName);
        var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;

        // Use SavePattern if provided, otherwise use SaveName or dirName
        var saveName = dirName;
        if (!string.IsNullOrWhiteSpace(DownloaderConfig.MyOptions.SavePattern))
        {
            saveName = OtherUtil.FormatSavePattern(DownloaderConfig.MyOptions.SavePattern, streamSpec, DownloaderConfig.MyOptions.SaveName, task.Id);
        }
        else if (DownloaderConfig.MyOptions.SaveName != null)
        {
            saveName = $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.');
        }
        var headers = DownloaderConfig.Headers;

        var decryptionBinaryPath = DownloaderConfig.MyOptions.DecryptionBinaryPath!;
        var decryptEngine = DownloaderConfig.MyOptions.DecryptionEngine;
        var mp4InitFile = "";
        var currentKID = "";
        var readInfo = false; // 是否读取过
        var mp4Info = new ParsedMP4Info();

        // 用户自定义范围导致被跳过的时长 计算字幕偏移使用
        var skippedDur = streamSpec.SkippedDuration ?? 0d;

        Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}");

        // 创建文件夹
        if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
        if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);

        var totalCount = segments.Count();
        if (streamSpec.Playlist?.MediaInit != null)
        {
            totalCount++;
        }

        task.MaxValue = totalCount;
        task.StartTask();

        // 开始下载
        Logger.InfoMarkUp(ResString.startDownloading + streamSpec.ToShortString());

        // 对于CENC,全部自动开启二进制合并
        if (!DownloaderConfig.MyOptions.BinaryMerge && totalCount >= 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method == Common.Enum.EncryptMethod.CENC)
        {
            DownloaderConfig.MyOptions.BinaryMerge = true;
            Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge4}[/]");
        }

        // 下载init
        if (streamSpec.Playlist?.MediaInit != null)
        {
            // 对于fMP4,自动开启二进制合并
            if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES)
            {
                DownloaderConfig.MyOptions.BinaryMerge = true;
                Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]");
            }

            var path = Path.Combine(tmpDir, "_init.mp4.tmp");
            var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
            FileDic[streamSpec.Playlist.MediaInit] = result;
            if (result is not { Success: true })
            {
                throw new Exception("Download init file failed!");
            }
            mp4InitFile = result.ActualFilePath;
            task.Increment(1);

            // 读取mp4信息
            if (result is { Success: true }) 
            {
                mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath);
                currentKID = mp4Info.KID;
                // try shaka packager, which can handle WebM
                if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {
                    currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, decryptionBinaryPath);
                }
                // 从文件读取KEY
                await SearchKeyAsync(currentKID);
                // 实时解密
                if ((streamSpec.Playlist.MediaInit.IsEncrypted || !string.IsNullOrEmpty(currentKID)) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS)
                {
                    var enc = result.ActualFilePath;
                    var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
                    var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM);
                    if (dResult)
                    {
                        FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec;
                    }
                }
                // ffmpeg读取信息
                if (!readInfo)
                {
                    Logger.WarnMarkUp(ResString.readingInfo);
                    mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath);
                    mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));
                    ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter);
                    readInfo = true;
                }
            }
        }

        // 计算填零个数
        var pad = "0".PadLeft(segments.Count().ToString().Length, '0');

        // 下载第一个分片
        if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS)
        {
            var seg = segments.First();
            segments = segments.Skip(1);

            var index = seg.Index;
            var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
            var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
            FileDic[seg] = result;
            if (result is not { Success: true })
            {
                throw new Exception("Download first segment failed!");
            }
            task.Increment(1);
            if (result is { Success: true })
            {
                // 修复MSS init
                if (StreamExtractor.ExtractorType == ExtractorType.MSS)
                {
                    var processor = new MSSMoovProcessor(streamSpec);
                    var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath));
                    await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header);
                    if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))
                    {
                        // 需要重新解密init
                        var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath;
                        var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
                        var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);
                        if (dResult)
                        {
                            FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec;
                        }
                    }
                }
                // 读取init信息
                if (string.IsNullOrEmpty(currentKID))
                {
                    currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;
                }
                // try shaka packager, which can handle WebM
                if (string.IsNullOrEmpty(currentKID) &&  DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {
                    currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, decryptionBinaryPath);
                }
                // 从文件读取KEY
                await SearchKeyAsync(currentKID);
                // 实时解密
                if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))
                {
                    var enc = result.ActualFilePath;
                    var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
                    mp4Info = MP4DecryptUtil.GetMP4Info(enc);
                    var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM);
                    if (dResult)
                    {
                        File.Delete(enc);
                        result.ActualFilePath = dec;
                    }
                }
                if (!readInfo)
                {
                    // ffmpeg读取信息
                    Logger.WarnMarkUp(ResString.readingInfo);
                    mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath);
                    mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));
                    ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter);
                    readInfo = true;
                }
            }
        }

        // 开始下载
        var options = new ParallelOptions()
        {
            MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount
        };
        await Parallel.ForEachAsync(segments, options, async (seg, _) =>
        {
            var index = seg.Index;
            var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
            var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
            FileDic[seg] = result;
            if (result is { Success: true })
                task.Increment(1);
            // 实时解密
            if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result is { Success: true } && !string.IsNullOrEmpty(currentKID)) 
            {
                var enc = result.ActualFilePath;
                var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
                mp4Info = MP4DecryptUtil.GetMP4Info(enc);
                var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM);
                if (dResult)
                {
                    File.Delete(enc);
                    result.ActualFilePath = dec;
                }
            }
        });

        // 修改输出后缀
        var outputExt = "." + streamSpec.Extension;
        if (streamSpec.Extension == null) outputExt = ".ts";
        else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: "m4s" or "mp4" }) outputExt = ".m4a";
        else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension is "m4s" or "mp4") outputExt = ".mp4";

        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES)
        {
            outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? ".srt" : ".vtt";
        }
        var output = Path.Combine(saveDir, saveName + outputExt);

        // 检测目标文件是否存在,使用智能重命名
        var finalOutput = OtherUtil.HandleFileCollision(output, streamSpec);
        if (finalOutput != output)
        {
            Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(finalOutput)}");
            output = finalOutput;
        }

        if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, Keys.Length: > 0 } && mp4InitFile != "")
        {
            File.Delete(mp4InitFile);
            // shaka/ffmpeg实时解密不需要init文件用于合并
            if (decryptEngine != DecryptEngine.MP4DECRYPT)
            {
                FileDic!.Remove(streamSpec.Playlist!.MediaInit, out _);
            }
        }

        // 校验分片数量
        if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null))
        {
            Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Count(s => s != null));
            return false;
        }

        // 移除无效片段
        var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key);
        foreach (var badKey in badKeys)
        {
            FileDic!.Remove(badKey, out _);
        }

        // 校验完整性
        if (DownloaderConfig.CheckContentLength && FileDic.Values.Any(a => a!.Success == false)) 
        {
            return false;
        }

        // 自动修复VTT raw字幕
        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("vtt")) 
        {
            Logger.WarnMarkUp(ResString.fixingVTT);
            // 排序字幕并修正时间戳
            bool first = true;
            var finalVtt = new WebVttSub();
            var keys = FileDic.Keys.OrderBy(k => k.Index);
            foreach (var seg in keys)
            {
                var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath);
                var vtt = WebVttSub.Parse(vttContent);
                // 手动计算MPEGTS
                if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
                {
                    vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
                }
                if (first) { finalVtt = vtt; first = false; }
                else finalVtt.AddCuesFromOne(vtt);
            }
            // 写出字幕
            var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
            foreach (var item in files) File.Delete(item);
            FileDic.Clear();
            var index = 0;
            var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
            // 设置字幕偏移
            finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
            var subContentFixed = finalVtt.ToVtt();
            // 转换字幕格式
            if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
            {
                path = Path.ChangeExtension(path, ".srt");
                subContentFixed = finalVtt.ToSrt();
            }
            await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
            FileDic[keys.First()] = new DownloadResult()
            {
                ActualContentLength = subContentFixed.Length,
                ActualFilePath = path
            };
        }

        // 自动修复VTT mp4字幕
        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
                                                       && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s"))
        {
            var initFile = FileDic.Values.FirstOrDefault(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init"));
            var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
            var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
            if (sawVtt)
            {
                Logger.WarnMarkUp(ResString.fixingVTTmp4);
                var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray();
                var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale);
                // 写出字幕
                var firstKey = FileDic.Keys.First();
                var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
                foreach (var item in files) File.Delete(item);
                FileDic.Clear();
                var index = 0;
                var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
                // 设置字幕偏移
                finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
                var subContentFixed = finalVtt.ToVtt();
                // 转换字幕格式
                if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
                {
                    path = Path.ChangeExtension(path, ".srt");
                    subContentFixed = finalVtt.ToSrt();
                }
                await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
                FileDic[firstKey] = new DownloadResult()
                {
                    ActualContentLength = subContentFixed.Length,
                    ActualFilePath = path
                };
            }
        }

        // 自动修复TTML raw字幕
        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("ttml"))
        {
            Logger.WarnMarkUp(ResString.fixingTTML);
            var first = true;
            var finalVtt = new WebVttSub();
            var keys = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Key);
            foreach (var seg in keys)
            {
                var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0);
                // 手动计算MPEGTS
                if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
                {
                    vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
                }
                if (first) { finalVtt = vtt; first = false; }
                else finalVtt.AddCuesFromOne(vtt);
            }
            // 写出字幕
            var firstKey = FileDic.Keys.First();
            var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();

            // 处理图形字幕
            await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir);

            var keepSegments = OtherUtil.GetEnvironmentVariable(EnvConfigKey.ReKeepImageSegments);
            if (keepSegments != "1")
                foreach (var item in files) File.Delete(item);
            FileDic.Clear();
            var index = 0;
            var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
            // 设置字幕偏移
            finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
            var subContentFixed = finalVtt.ToVtt();
            // 转换字幕格式
            if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
            {
                path = Path.ChangeExtension(path, ".srt");
                subContentFixed = finalVtt.ToSrt();
            }
            await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
            FileDic[firstKey] = new DownloadResult()
            {
                ActualContentLength = subContentFixed.Length,
                ActualFilePath = path
            };
        }

        // 自动修复TTML mp4字幕
        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("m4s")
            && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) 
        {
            Logger.WarnMarkUp(ResString.fixingTTMLmp4);
            // sawTtml暂时不判断
            // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
            // var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
            // var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);
            var first = true;
            var finalVtt = new WebVttSub();
            var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key);
            foreach (var seg in keys)
            {
                var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0);
                // 手动计算MPEGTS
                if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
                {
                    vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
                }
                if (first) { finalVtt = vtt; first = false; }
                else finalVtt.AddCuesFromOne(vtt);
            }

            // 写出字幕
            var firstKey = FileDic.Keys.First();
            var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();

            // 处理图形字幕
            await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir);

            var keepSegments = OtherUtil.GetEnvironmentVariable(EnvConfigKey.ReKeepImageSegments);
            if (keepSegments != "1")
                foreach (var item in files) File.Delete(item);
            FileDic.Clear();
            var index = 0;
            var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
            // 设置字幕偏移
            finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
            var subContentFixed = finalVtt.ToVtt();
            // 转换字幕格式
            if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
            {
                path = Path.ChangeExtension(path, ".srt");
                subContentFixed = finalVtt.ToSrt();
            }
            await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);
            FileDic[firstKey] = new DownloadResult()
            {
                ActualContentLength = subContentFixed.Length,
                ActualFilePath = path
            };
        }

        bool mergeSuccess = false;
        // 合并
        if (!DownloaderConfig.MyOptions.SkipMerge)
        {
            // 字幕也使用二进制合并
            if (DownloaderConfig.MyOptions.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES)
            {
                Logger.InfoMarkUp(ResString.binaryMerge);
                var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
                MergeUtil.CombineMultipleFilesIntoSingleFile(files, output);
                mergeSuccess = true;
            }
            else
            {
                // ffmpeg合并
                var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
                Logger.InfoMarkUp(ResString.ffmpegMerge);
                var ext = streamSpec.MediaType == MediaType.AUDIO ? "m4a" : "mp4";
                var ffOut = Path.Combine(Path.GetDirectoryName(output)!, Path.GetFileNameWithoutExtension(output) + $".{ext}");
                // 检测目标文件是否存在,使用智能重命名
                var finalFfOut = OtherUtil.HandleFileCollision(ffOut, streamSpec);
                if (finalFfOut != ffOut)
                {
                    Logger.WarnMarkUp($"{Path.GetFileName(ffOut)} => {Path.GetFileName(finalFfOut)}");
                    ffOut = finalFfOut;
                }
                // 大于1800分片,需要分步骤合并
                if (files.Length >= 1800)
                {
                    Logger.WarnMarkUp(ResString.partMerge);
                    files = MergeUtil.PartialCombineMultipleFiles(files);
                    FileDic.Clear();
                    foreach (var item in files)
                    {
                        FileDic[new MediaSegment() { Url = item }] = new DownloadResult()
                        {
                            ActualFilePath = item
                        };
                    }
                }
                mergeSuccess = MergeUtil.MergeByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, files, Path.ChangeExtension(ffOut, null), ext, useAACFilter, writeDate: !DownloaderConfig.MyOptions.NoDateInfo, useConcatDemuxer: DownloaderConfig.MyOptions.UseFFmpegConcatDemuxer);
                if (mergeSuccess) output = ffOut;
            }
        }

        // 删除临时文件夹
        if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && mergeSuccess)
        {
            var files = FileDic.Values.Select(v => v!.ActualFilePath);
            foreach (var file in files)
            {
                File.Delete(file);
            }
            OtherUtil.SafeDeleteDir(tmpDir);
        }

        // 重新读取init信息
        if (mergeSuccess && totalCount >= 1 && string.IsNullOrEmpty(currentKID) && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE)
        {
            currentKID = MP4DecryptUtil.GetMP4Info(output).KID;
            // try shaka packager, which can handle WebM
            if (string.IsNullOrEmpty(currentKID) &&  DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {
                currentKID = MP4DecryptUtil.ReadInitShaka(output, decryptionBinaryPath);
            }
            // 从文件读取KEY
            await SearchKeyAsync(currentKID);
        }

        // 调用mp4decrypt解密
        if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions is { MP4RealTimeDecryption: false, Keys.Length: > 0 })
        {
            var enc = output;
            var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
            mp4Info = MP4DecryptUtil.GetMP4Info(enc);
            Logger.InfoMarkUp($"[grey]Decrypting using {decryptEngine}...[/]");
            var result = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM);
            if (result)
            {
                File.Delete(enc);
                File.Move(dec, enc);
            }
        }

        // 记录所有文件信息
        if (File.Exists(output))
        {
            OutputFiles.Add(new OutputFile()
            {
                Index = task.Id,
                FilePath = output,
                LangCode = streamSpec.Language,
                Description = streamSpec.Name,
                Mediainfos = mediaInfos,
                MediaType = streamSpec.MediaType,
            });
        }

        return true;
    }

    public async Task<bool> StartDownloadAsync()
    {
        ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算
        ConcurrentDictionary<StreamSpec, bool?> Results = new();
            
        var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
        progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;

        // 进度条的列定义
        var progressColumns = new ProgressColumn[]
        {
            new TaskDescriptionColumn() { Alignment = Justify.Left },
            new ProgressBarColumn(){ Width = 30 },
            new MyPercentageColumn(),
            new DownloadStatusColumn(SpeedContainerDic),
            new DownloadSpeedColumn(SpeedContainerDic), // 速度计算
            new RemainingTimeColumn(),
            new SpinnerColumn(),
        };
        if (DownloaderConfig.MyOptions.NoAnsiColor)
        {
            progressColumns = progressColumns.SkipLast(1).ToArray();
        }
        progress.Columns(progressColumns);

        if (DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, DecryptionEngine: not DecryptEngine.SHAKA_PACKAGER, Keys.Length: > 0 })
            Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]");

        await progress.StartAsync(async ctx =>
        {
            // 创建任务
            var dic = SelectedSteams.Select(item =>
            {
                var description = item.ToShortShortString();
                var task = ctx.AddTask(description, autoStart: false);
                SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算
                // 限速设置
                if (DownloaderConfig.MyOptions.MaxSpeed != null)
                {
                    SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value;
                }
                return (item, task);
            }).ToDictionary(item => item.item, item => item.task);

            if (!DownloaderConfig.MyOptions.ConcurrentDownload)
            {
                // 遍历,顺序下载
                foreach (var kp in dic)
                {
                    var task = kp.Value;
                    var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);
                    Results[kp.Key] = result;
                    // 失败不再下载后续
                    if (!result) break;
                }
            }
            else
            {
                // 并发下载
                await Parallel.ForEachAsync(dic, async (kp, _) =>
                {
                    var task = kp.Value;
                    var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);
                    Results[kp.Key] = result;
                });
            }
        });

        var success = Results.Values.All(v => v == true);

        // 删除临时文件夹
        if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && success)
        {
            foreach (var item in StreamExtractor.RawFiles)
            {
                var file = Path.Combine(DownloaderConfig.DirPrefix, item.Key);
                if (File.Exists(file)) File.Delete(file);
            }
            OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix);
        }

        // 混流
        if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) 
        {
            OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList();
            // 是否跳过字幕
            if (DownloaderConfig.MyOptions.MuxOptions!.SkipSubtitle)
            {
                OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList();
            }
            if (DownloaderConfig.MyOptions.MuxImports != null)
            {
                OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports);
            }
            OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]"));
            var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
            var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat);
            var dirName = Path.GetFileName(DownloaderConfig.DirPrefix);
            var outName = $"{dirName}.MUX";
            var outPath = Path.Combine(saveDir, outName);
            Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]");
            var result = false;
            if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath);
            else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo);
            // 完成后删除各轨道文件
            if (result)
            {
                if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles)
                {
                    Logger.WarnMarkUp("[grey]Cleaning files...[/]");
                    OutputFiles.ForEach(f => File.Delete(f.FilePath));
                    var tmpDir = DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory;
                    OtherUtil.SafeDeleteDir(tmpDir);
                }
            }
            else
            {
                success = false;
                Logger.ErrorMarkUp($"Mux failed");
            }
            // 判断是否要改名
            var newPath = Path.ChangeExtension(outPath, ext);
            if (result && !File.Exists(newPath))
            {
                Logger.WarnMarkUp($"Rename to [grey]{Path.GetFileName(newPath).EscapeMarkup()}[/]");
                File.Move(outPath + ext, newPath);
            }
        }

        return success;
    }
}

================================================
FILE: src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs
================================================
using Mp4SubtitleParser;
using N_m3u8DL_RE.Column;
using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Config;
using N_m3u8DL_RE.Downloader;
using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Parser;
using N_m3u8DL_RE.Parser.Mp4;
using N_m3u8DL_RE.Util;
using Spectre.Console;
using System.Collections.Concurrent;
using System.IO.Pipes;
using System.Text;
using System.Threading.Tasks.Dataflow;
using N_m3u8DL_RE.Enum;

namespace N_m3u8DL_RE.DownloadManager;

internal class SimpleLiveRecordManager2
{
    IDownloader Downloader;
    DownloaderConfig DownloaderConfig;
    StreamExtractor StreamExtractor;
    List<StreamSpec> SelectedSteams;
    ConcurrentDictionary<int, string> PipeSteamNamesDic = new();
    List<OutputFile> OutputFiles = [];
    DateTime? PublishDateTime;
    bool STOP_FLAG = false;
    int WAIT_SEC = 0; // 刷新间隔
    ConcurrentDictionary<int, int> RecordedDurDic = new(); // 已录制时长
    ConcurrentDictionary<int, int> RefreshedDurDic = new(); // 已刷新出的时长
    ConcurrentDictionary<int, BufferBlock<List<MediaSegment>>> BlockDic = new(); // 各流的Block
    ConcurrentDictionary<int, bool> SamePathDic = new(); // 各流是否allSamePath
    ConcurrentDictionary<int, bool> RecordLimitReachedDic = new(); // 各流是否达到上限
    ConcurrentDictionary<int, string> LastFileNameDic = new(); // 上次下载的文件名
    ConcurrentDictionary<int, long> MaxIndexDic = new(); // 最大Index
    ConcurrentDictionary<int, long> DateTimeDic = new(); // 上次下载的dateTime
    CancellationTokenSource CancellationTokenSource = new(); // 取消Wait

    private readonly Lock lockObj = new();
    TimeSpan? audioStart = null;

    public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
    {
        this.DownloaderConfig = downloaderConfig;
        Downloader = new SimpleDownloader(DownloaderConfig);
        PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;
        StreamExtractor = streamExtractor;
        SelectedSteams = selectedSteams;
    }

    // 从文件读取KEY
    private async Task SearchKeyAsync(string? currentKID)
    {
        var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID);
        if (_key != null)
        {
            if (DownloaderConfig.MyOptions.Keys == null)
                DownloaderConfig.MyOptions.Keys = [_key];
            else
                DownloaderConfig.MyOptions.Keys = [..DownloaderConfig.MyOptions.Keys, _key];
        }
    }

    /// <summary>
    /// 获取时间戳
    /// </summary>
    /// <param name="dateTime"></param>
    /// <returns></returns>
    private long GetUnixTimestamp(DateTime dateTime)
    {
        return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds();
    }

    /// <summary>
    /// 获取分段文件夹
    /// </summary>
    /// <param name="segment"></param>
    /// <param name="allHasDatetime"></param>
    /// <returns></returns>
    private string GetSegmentName(MediaSegment segment, bool allHasDatetime, bool allSamePath)
    {
        if (!string.IsNullOrEmpty(segment.NameFromVar))
        {
            return segment.NameFromVar;
        }

        bool hls = StreamExtractor.ExtractorType == ExtractorType.HLS;

        string name = OtherUtil.GetFileNameFromInput(segment.Url, false);
        if (allSamePath)
        {
            name = OtherUtil.GetValidFileName(segment.Url.Split('?').Last(), "_");
        }

        if (hls && allHasDatetime)
        {
            name = GetUnixTimestamp(segment.DateTime!.Value).ToString();
        }
        else if (hls)
        {
            name = segment.Index.ToString();
        }

        return name;
    }

    private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)
    {
        if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))
        {
            DownloaderConfig.MyOptions.BinaryMerge = true;
            Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]");
        }

        if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))
        {
            DownloaderConfig.MyOptions.MuxAfterDone = false;
            Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
        }

        if (mediainfos.Where(m => m.Type == "Audio").All(m => m.BaseInfo!.Contains("aac")))
        {
            useAACFilter = true;
        }

        if (mediainfos.All(m => m.Type == "Audio") && streamSpec.MediaType != MediaType.AUDIO)
        {
            streamSpec.MediaType = MediaType.AUDIO;
        }
        else if (mediainfos.All(m => m.Type == "Subtitle") && streamSpec.MediaType != MediaType.SUBTITLES)
        {
            streamSpec.MediaType = MediaType.SUBTITLES;

            if (streamSpec.Extension is null or "ts")
                streamSpec.Extension = "vtt";
        }
    }

    private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer, BufferBlock<List<MediaSegment>> source)
    {
        var baseTimestamp = PublishDateTime == null ? 0L : (long)(PublishDateTime.Value.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds;
        var decryptionBinaryPath = DownloaderConfig.MyOptions.DecryptionBinaryPath!;
        var mp4InitFile = "";
        var currentKID = "";
        var readInfo = false; // 是否读取过
        bool useAACFilter = false; // ffmpeg合并flag
        bool initDownloaded = false; // 是否下载过init文件
        ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
        List<Mediainfo> mediaInfos = [];
        Stream? fileOutputStream = null;
        WebVttSub currentVtt = new(); // 字幕流始终维护一个实例
        bool firstSub = true;
        task.StartTask();

        var name = streamSpec.ToShortString();
        var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO;
        var dirName = $"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}";
        var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName);
        var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;

        // Use SavePattern if provided, otherwise use SaveName or dirName
        var saveName = dirName;
        if (!string.IsNullOrWhiteSpace(DownloaderConfig.MyOptions.SavePattern))
        {
            saveName = OtherUtil.FormatSavePattern(DownloaderConfig.MyOptions.SavePattern, streamSpec, DownloaderConfig.MyOptions.SaveName, task.Id);
        }
        else if (DownloaderConfig.MyOptions.SaveName != null)
        {
            saveName = $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.');
        }
        var headers = DownloaderConfig.Headers;
        var decryptEngine = DownloaderConfig.MyOptions.DecryptionEngine;

        Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}");

        // 创建文件夹
        if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
        if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);

        while (true && await source.OutputAvailableAsync())
        {
            // 接收新片段 且总是拿全部未处理的片段
            // 有时每次只有很少的片段,但是之前的片段下载慢,导致后面还没下载的片段都失效了
            // TryReceiveAll可以稍微缓解一下
            source.TryReceiveAll(out IList<List<MediaSegment>>? segmentsList);
            var segments = segmentsList!.SelectMany(s => s);
            if (segments == null || !segments.Any()) continue;
            var segmentsDuration = segments.Sum(s => s.Duration);
            Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false))));

            // 下载init
            if (!initDownloaded && streamSpec.Playlist?.MediaInit != null) 
            {
                task.MaxValue += 1;
                // 对于fMP4,自动开启二进制合并
                if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES)
                {
                    DownloaderConfig.MyOptions.BinaryMerge = true;
                    Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge}[/]");
                }

                var path = Path.Combine(tmpDir, "_init.mp4.tmp");
                var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
                FileDic[streamSpec.Playlist.MediaInit] = result;
                if (result is not { Success: true })
                {
                    throw new Exception("Download init file failed!");
                }
                mp4InitFile = result.ActualFilePath;
                task.Increment(1);

                // 读取mp4信息
                if (result is { Success: true })
                {
                    currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;
                    // 从文件读取KEY
                    await SearchKeyAsync(currentKID);
                    // 实时解密
                    if ((streamSpec.Playlist.MediaInit.IsEncrypted || !string.IsNullOrEmpty(currentKID)) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS)
                    {
                        var enc = result.ActualFilePath;
                        var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
                        var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);
                        if (dResult)
                        {
                            FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec;
                        }
                    }
                    // ffmpeg读取信息
                    if (!readInfo)
                    {
                        Logger.WarnMarkUp(ResString.readingInfo);
                        mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath);
                        mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));
                        lock (lockObj)
                        {
                            if (audioStart == null) audioStart = mediaInfos.FirstOrDefault(x => x.Type == "Audio")?.StartTime;
                        }
                        ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter);
         
Download .txt
gitextract_dnlao_8d/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── build_latest.yml
├── .gitignore
├── LICENSE
├── README.en.md
├── README.md
├── TestStreams.md
└── src/
    ├── N_m3u8DL-RE/
    │   ├── Column/
    │   │   ├── DownloadSpeedColumn.cs
    │   │   ├── DownloadStatusColumn.cs
    │   │   ├── MyPercentageColumn.cs
    │   │   ├── RecordingDurationColumn.cs
    │   │   ├── RecordingSizeColumn.cs
    │   │   └── RecordingStatusColumn.cs
    │   ├── CommandLine/
    │   │   ├── CommandInvoker.cs
    │   │   ├── ComplexParamParser.cs
    │   │   └── MyOption.cs
    │   ├── Config/
    │   │   ├── DownloaderConfig.cs
    │   │   └── EnvConfigKey.cs
    │   ├── Crypto/
    │   │   ├── AESUtil.cs
    │   │   ├── CSChaCha20.cs
    │   │   └── ChaCha20Util.cs
    │   ├── Directory.Build.props
    │   ├── DownloadManager/
    │   │   ├── HTTPLiveRecordManager.cs
    │   │   ├── SimpleDownloadManager.cs
    │   │   └── SimpleLiveRecordManager2.cs
    │   ├── Downloader/
    │   │   ├── IDownloader.cs
    │   │   └── SimpleDownloader.cs
    │   ├── Entity/
    │   │   ├── CustomRange.cs
    │   │   ├── DownloadResult.cs
    │   │   ├── Mediainfo.cs
    │   │   ├── MuxOptions.cs
    │   │   ├── OutputFile.cs
    │   │   ├── SpeedContainer.cs
    │   │   └── StreamFilter.cs
    │   ├── Enum/
    │   │   ├── DecryptEngine.cs
    │   │   ├── MuxFormat.cs
    │   │   └── SubtitleFormat.cs
    │   ├── N_m3u8DL-RE.csproj
    │   ├── Processor/
    │   │   ├── DemoProcessor.cs
    │   │   ├── DemoProcessor2.cs
    │   │   └── NowehoryzontyUrlProcessor.cs
    │   ├── Program.cs
    │   └── Util/
    │       ├── CultureUtil.cs
    │       ├── DownloadUtil.cs
    │       ├── FilterUtil.cs
    │       ├── ImageHeaderUtil.cs
    │       ├── LanguageCodeUtil.cs
    │       ├── LargeSingleFileSplitUtil.cs
    │       ├── MP4DecryptUtil.cs
    │       ├── MediainfoUtil.cs
    │       ├── MergeUtil.cs
    │       ├── OtherUtil.cs
    │       ├── PipeUtil.cs
    │       └── SubtitleUtil.cs
    ├── N_m3u8DL-RE.Common/
    │   ├── Entity/
    │   │   ├── EncryptInfo.cs
    │   │   ├── MSSData.cs
    │   │   ├── MediaPart.cs
    │   │   ├── MediaSegment.cs
    │   │   ├── Playlist.cs
    │   │   ├── StreamSpec.cs
    │   │   ├── SubCue.cs
    │   │   └── WebVttSub.cs
    │   ├── Enum/
    │   │   ├── Choise.cs
    │   │   ├── EncryptMethod.cs
    │   │   ├── ExtractorType.cs
    │   │   ├── MediaType.cs
    │   │   └── RoleType.cs
    │   ├── JsonContext/
    │   │   └── JsonContext.cs
    │   ├── JsonConverter/
    │   │   └── BytesBase64Converter.cs
    │   ├── Log/
    │   │   ├── CustomAnsiConsole.cs
    │   │   ├── LogLevel.cs
    │   │   └── Logger.cs
    │   ├── N_m3u8DL-RE.Common.csproj
    │   ├── Resource/
    │   │   ├── ResString.cs
    │   │   ├── StaticText.cs
    │   │   └── TextContainer.cs
    │   └── Util/
    │       ├── BinaryContentCheckUtil.cs
    │       ├── GlobalUtil.cs
    │       ├── HTTPUtil.cs
    │       ├── HexUtil.cs
    │       └── RetryUtil.cs
    ├── N_m3u8DL-RE.Parser/
    │   ├── Config/
    │   │   └── ParserConfig.cs
    │   ├── Constants/
    │   │   ├── DASHTags.cs
    │   │   ├── HLSTags.cs
    │   │   └── MSSTags.cs
    │   ├── Extractor/
    │   │   ├── DASHExtractor2.cs
    │   │   ├── HLSExtractor.cs
    │   │   ├── IExtractor.cs
    │   │   ├── LiveTSExtractor.cs
    │   │   └── MSSExtractor.cs
    │   ├── InternalsVisibleTo.cs
    │   ├── Mp4/
    │   │   ├── BinaryReader2.cs
    │   │   ├── BinaryWriter2.cs
    │   │   ├── MP4InitUtil.cs
    │   │   ├── MP4Parser.cs
    │   │   ├── MP4TtmlUtil.cs
    │   │   ├── MP4VttUtil.cs
    │   │   └── MSSMoovProcessor.cs
    │   ├── N_m3u8DL-RE.Parser.csproj
    │   ├── Processor/
    │   │   ├── ContentProcessor.cs
    │   │   ├── DASH/
    │   │   │   └── DefaultDASHContentProcessor.cs
    │   │   ├── DefaultUrlProcessor.cs
    │   │   ├── HLS/
    │   │   │   ├── DefaultHLSContentProcessor.cs
    │   │   │   └── DefaultHLSKeyProcessor.cs
    │   │   ├── KeyProcessor.cs
    │   │   └── UrlProcessor.cs
    │   ├── StreamExtractor.cs
    │   └── Util/
    │       └── ParserUtil.cs
    ├── N_m3u8DL-RE.Tests/
    │   ├── Common/
    │   │   └── Util/
    │   │       └── HexUtilTests.cs
    │   ├── N_m3u8DL-RE.Tests.csproj
    │   ├── Parser/
    │   │   └── Extractor/
    │   │       └── DASHExtractor2Tests.cs
    │   ├── ResourceHelper.cs
    │   └── Resources/
    │       └── Dash/
    │           └── Manifest_1080p.mpd
    └── N_m3u8DL-RE.sln
Download .txt
SYMBOL INDEX (522 symbols across 99 files)

FILE: src/N_m3u8DL-RE.Common/Entity/EncryptInfo.cs
  class EncryptInfo (line 5) | public class EncryptInfo
    method EncryptInfo (line 15) | public EncryptInfo() { }
    method EncryptInfo (line 21) | public EncryptInfo(string method)
    method ParseMethod (line 26) | public static EncryptMethod ParseMethod(string? method)

FILE: src/N_m3u8DL-RE.Common/Entity/MSSData.cs
  class MSSData (line 3) | public class MSSData

FILE: src/N_m3u8DL-RE.Common/Entity/MediaPart.cs
  class MediaPart (line 4) | public class MediaPart

FILE: src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs
  class MediaSegment (line 5) | public class MediaSegment
    method Equals (line 24) | public override bool Equals(object? obj)
    method GetHashCode (line 36) | public override int GetHashCode()

FILE: src/N_m3u8DL-RE.Common/Entity/Playlist.cs
  class Playlist (line 3) | public class Playlist

FILE: src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs
  class StreamSpec (line 7) | public class StreamSpec
    method ToShortString (line 66) | public string ToShortString()
    method ToShortShortString (line 100) | public string ToShortShortString()
    method ToString (line 134) | public override string ToString()

FILE: src/N_m3u8DL-RE.Common/Entity/SubCue.cs
  class SubCue (line 3) | public class SubCue
    method Equals (line 10) | public override bool Equals(object? obj)
    method GetHashCode (line 19) | public override int GetHashCode()

FILE: src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs
  class WebVttSub (line 6) | public partial class WebVttSub
    method TSMapRegex (line 8) | [GeneratedRegex("X-TIMESTAMP-MAP.*")]
    method TSValueRegex (line 10) | [GeneratedRegex("MPEGTS:(\\d+)")]
    method SplitRegex (line 12) | [GeneratedRegex("\\s")]
    method VttClassRegex (line 14) | [GeneratedRegex(@"<c\..*?>([\s\S]*?)<\/c>")]
    method Parse (line 25) | public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L)
    method Parse (line 36) | public static WebVttSub Parse(byte[] textBytes, Encoding encoding, lon...
    method Parse (line 46) | public static WebVttSub Parse(string text, long BaseTimestamp = 0L)
    method RemoveClassTag (line 119) | private static string RemoveClassTag(string text)
    method AddCuesFromOne (line 136) | public WebVttSub AddCuesFromOne(WebVttSub webSub)
    method FixTimestamp (line 157) | private void FixTimestamp(WebVttSub sub, long baseTimestamp)
    method GetCues (line 182) | private IEnumerable<SubCue> GetCues()
    method ConvertToTS (line 187) | private static TimeSpan ConvertToTS(string str)
    method ToString (line 212) | public override string ToString()
    method LeftShiftTime (line 229) | public void LeftShiftTime(TimeSpan time)
    method ToVtt (line 241) | public string ToVtt()
    method ToSrt (line 246) | public string ToSrt()

FILE: src/N_m3u8DL-RE.Common/Enum/Choise.cs
  type Choise (line 3) | public enum Choise

FILE: src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs
  type EncryptMethod (line 3) | public enum EncryptMethod

FILE: src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs
  type ExtractorType (line 3) | public enum ExtractorType

FILE: src/N_m3u8DL-RE.Common/Enum/MediaType.cs
  type MediaType (line 3) | public enum MediaType

FILE: src/N_m3u8DL-RE.Common/Enum/RoleType.cs
  type RoleType (line 3) | public enum RoleType

FILE: src/N_m3u8DL-RE.Common/JsonContext/JsonContext.cs
  class JsonContext (line 7) | [JsonSourceGenerationOptions(

FILE: src/N_m3u8DL-RE.Common/JsonConverter/BytesBase64Converter.cs
  class BytesBase64Converter (line 6) | internal class BytesBase64Converter : JsonConverter<byte[]>
    method Read (line 8) | public override byte[] Read(ref Utf8JsonReader reader, Type typeToConv...
    method Write (line 10) | public override void Write(Utf8JsonWriter writer, byte[] value, JsonSe...

FILE: src/N_m3u8DL-RE.Common/Log/CustomAnsiConsole.cs
  class NonAnsiWriter (line 7) | public partial class NonAnsiWriter : TextWriter
    method Write (line 13) | public override void Write(char value)
    method Write (line 18) | public override void Write(string? value)
    method RemoveAnsiEscapeSequences (line 28) | private void RemoveAnsiEscapeSequences(string? input)
    method MyRegex (line 41) | [GeneratedRegex(@"\x1B\[(\d+;?)+m")]
    method MyRegex1 (line 43) | [GeneratedRegex(@"\[\??\d+[AKlh]")]
    method MyRegex2 (line 45) | [GeneratedRegex("[\r\n] +")]
  class CustomAnsiConsole (line 52) | public static class CustomAnsiConsole
    method InitConsole (line 56) | public static void InitConsole(bool forceAnsi, bool noAnsiColor)
    method Markup (line 86) | public static void Markup(string value)
    method MarkupLine (line 95) | public static void MarkupLine(string value)

FILE: src/N_m3u8DL-RE.Common/Log/LogLevel.cs
  type LogLevel (line 3) | public enum LogLevel

FILE: src/N_m3u8DL-RE.Common/Log/Logger.cs
  class Logger (line 7) | public static partial class Logger
    method VarsRepRegex (line 9) | [GeneratedRegex("{}")]
    method InitLogFile (line 30) | public static void InitLogFile()
    method GetCurrTime (line 68) | private static string GetCurrTime()
    method HandleLog (line 73) | private static void HandleLog(string write, string subWrite = "")
    method ReplaceVars (line 111) | private static string ReplaceVars(string data, params object[] ps)
    method Info (line 121) | public static void Info(string data, params object[] ps)
    method InfoMarkUp (line 130) | public static void InfoMarkUp(string data, params object[] ps)
    method Debug (line 139) | public static void Debug(string data, params object[] ps)
    method DebugMarkUp (line 148) | public static void DebugMarkUp(string data, params object[] ps)
    method Warn (line 157) | public static void Warn(string data, params object[] ps)
    method WarnMarkUp (line 166) | public static void WarnMarkUp(string data, params object[] ps)
    method Error (line 175) | public static void Error(string data, params object[] ps)
    method ErrorMarkUp (line 184) | public static void ErrorMarkUp(string data, params object[] ps)
    method ErrorMarkUp (line 193) | public static void ErrorMarkUp(Exception exception)
    method Extra (line 209) | public static void Extra(string data, params object[] ps)

FILE: src/N_m3u8DL-RE.Common/Resource/ResString.cs
  class ResString (line 3) | public static class ResString
    method GetText (line 145) | private static string GetText(string key)

FILE: src/N_m3u8DL-RE.Common/Resource/StaticText.cs
  class StaticText (line 3) | internal static class StaticText

FILE: src/N_m3u8DL-RE.Common/Resource/TextContainer.cs
  class TextContainer (line 3) | internal class TextContainer
    method TextContainer (line 9) | public TextContainer(string zhCN, string zhTW, string enUS)

FILE: src/N_m3u8DL-RE.Common/Util/BinaryContentCheckUtil.cs
  class BinaryContentCheckUtil (line 3) | public static class BinaryContentCheckUtil
    method LooksLikeBinary (line 5) | public static bool LooksLikeBinary(ReadOnlySpan<byte> data)
    method GetUtf8SequenceLength (line 53) | private static int GetUtf8SequenceLength(byte b)
    method IsValidUtf8Sequence (line 62) | private static bool IsValidUtf8Sequence(ReadOnlySpan<byte> seq)
    method IsMpeg2TsBuffer (line 73) | public static bool IsMpeg2TsBuffer(ReadOnlySpan<byte> buffer)

FILE: src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs
  class GlobalUtil (line 8) | public static class GlobalUtil
    method ConvertToJson (line 19) | public static string ConvertToJson(object o)
    method FormatFileSize (line 40) | public static string FormatFileSize(double fileSize)
    method FormatTime (line 53) | public static string FormatTime(int time)
    method FindExecutable (line 66) | public static string? FindExecutable(string name)

FILE: src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs
  class HTTPUtil (line 9) | public static class HTTPUtil
    method DoGetAsync (line 26) | private static async Task<HttpResponseMessage> DoGetAsync(string url, ...
    method GetBytesAsync (line 76) | public static async Task<byte[]> GetBytesAsync(string url, Dictionary<...
    method GetWebSourceAsync (line 95) | public static async Task<string> GetWebSourceAsync(string url, Diction...
    method GetWebSourceAndNewUrlAsync (line 109) | public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(...
    method GetEncodingFromResponse (line 156) | private static Encoding? GetEncodingFromResponse(HttpResponseMessage r...
    method GetPostResponseAsync (line 173) | public static async Task<string> GetPostResponseAsync(string Url, byte...

FILE: src/N_m3u8DL-RE.Common/Util/HexUtil.cs
  class HexUtil (line 3) | public static class HexUtil
    method BytesToHex (line 5) | public static string BytesToHex(byte[] data, string split = "")
    method TryParseHexString (line 15) | public static bool TryParseHexString(string input, out byte[]? bytes)
    method TryParseBase64 (line 35) | public static bool TryParseBase64(string s, out string? key)
    method HexToBytes (line 49) | public static byte[] HexToBytes(string hex)

FILE: src/N_m3u8DL-RE.Common/Util/RetryUtil.cs
  class RetryUtil (line 7) | public static class RetryUtil
    method WebRequestRetryAsync (line 9) | public static async Task<T?> WebRequestRetryAsync<T>(Func<Task<T>> fun...

FILE: src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs
  class ParserConfig (line 8) | public class ParserConfig

FILE: src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs
  class DASHTags (line 3) | internal static class DASHTags

FILE: src/N_m3u8DL-RE.Parser/Constants/HLSTags.cs
  class HLSTags (line 3) | internal static class HLSTags

FILE: src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs
  class MSSTags (line 3) | internal static class MSSTags

FILE: src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs
  class DASHExtractor2 (line 14) | internal partial class DASHExtractor2 : IExtractor
    method DASHExtractor2 (line 25) | public DASHExtractor2(ParserConfig parserConfig)
    method SetInitUrl (line 32) | private void SetInitUrl()
    method ExtendBaseUrl (line 38) | private string ExtendBaseUrl(XElement element, string oriBaseUrl)
    method GetFrameRate (line 49) | private double? GetFrameRate(XElement element)
    method ExtractStreamsAsync (line 59) | public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
    method FilterLanguage (line 543) | private string? FilterLanguage(string? v)
    method RefreshPlayListAsync (line 549) | public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
    method ProcessUrlAsync (line 583) | private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
    method FetchPlayListAsync (line 607) | public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
    method PreProcessUrl (line 613) | public string PreProcessUrl(string url)
    method PreProcessContent (line 626) | public void PreProcessContent()
    method LangCodeRegex (line 637) | [GeneratedRegex(@"^[\w_\-\d]+$")]

FILE: src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs
  class HLSExtractor (line 12) | internal class HLSExtractor : IExtractor
    method HLSExtractor (line 23) | public HLSExtractor(ParserConfig parserConfig)
    method SetBaseUrl (line 30) | private void SetBaseUrl()
    method PreProcessContent (line 38) | public void PreProcessContent()
    method PreProcessUrl (line 58) | public string PreProcessUrl(string url)
    method ParseMasterListAsync (line 71) | private Task<List<StreamSpec>> ParseMasterListAsync()
    method ParseListAsync (line 205) | private Task<Playlist> ParseListAsync()
    method ParseKey (line 447) | private EncryptInfo ParseKey(string keyLine)
    method ExtractStreamsAsync (line 461) | public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
    method LoadM3u8FromUrlAsync (line 485) | private async Task LoadM3u8FromUrlAsync(string url)
    method RefreshUrlFromMaster (line 516) | private async Task RefreshUrlFromMaster(List<StreamSpec> lists)
    method FetchPlayListAsync (line 532) | public async Task FetchPlayListAsync(List<StreamSpec> lists)
    method RefreshPlayListAsync (line 569) | public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)

FILE: src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs
  type IExtractor (line 7) | public interface IExtractor
    method ExtractStreamsAsync (line 13) | Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
    method FetchPlayListAsync (line 15) | Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
    method RefreshPlayListAsync (line 16) | Task RefreshPlayListAsync(List<StreamSpec> streamSpecs);
    method PreProcessUrl (line 18) | string PreProcessUrl(string url);
    method PreProcessContent (line 20) | void PreProcessContent();

FILE: src/N_m3u8DL-RE.Parser/Extractor/LiveTSExtractor.cs
  class LiveTSExtractor (line 8) | internal class LiveTSExtractor : IExtractor
    method LiveTSExtractor (line 14) | public LiveTSExtractor(ParserConfig parserConfig)
    method ExtractStreamsAsync (line 19) | public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
    method FetchPlayListAsync (line 33) | public Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
    method PreProcessContent (line 38) | public void PreProcessContent()
    method PreProcessUrl (line 43) | public string PreProcessUrl(string url)
    method RefreshPlayListAsync (line 48) | public Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)

FILE: src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs
  class MSSExtractor (line 17) | internal partial class MSSExtractor : IExtractor
    method VCodecsRegex (line 19) | [GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")]
    method MSSExtractor (line 33) | public MSSExtractor(ParserConfig parserConfig)
    method SetInitUrl (line 39) | private void SetInitUrl()
    method ExtractStreamsAsync (line 45) | public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
    method ParseCodecs (line 263) | private static string? ParseCodecs(string fourCC, string? privateData)
    method ParseAVCCodecs (line 279) | private static string ParseAVCCodecs(string privateData)
    method ParseAACCodecs (line 285) | private static string ParseAACCodecs(string fourCC, string privateData)
    method FetchPlayListAsync (line 300) | public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
    method ProcessUrlAsync (line 306) | private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
    method PreProcessUrl (line 330) | public string PreProcessUrl(string url)
    method PreProcessContent (line 343) | public void PreProcessContent()
    method RefreshPlayListAsync (line 354) | public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)

FILE: src/N_m3u8DL-RE.Parser/Mp4/BinaryReader2.cs
  class BinaryReader2 (line 4) | class BinaryReader2 : BinaryReader
    method BinaryReader2 (line 6) | public BinaryReader2(System.IO.Stream stream) : base(stream) { }
    method HasMoreData (line 8) | public bool HasMoreData()
    method GetLength (line 13) | public long GetLength()
    method GetPosition (line 18) | public long GetPosition()
    method ReadInt32 (line 23) | public override int ReadInt32()
    method ReadInt16 (line 31) | public override short ReadInt16()
    method ReadInt64 (line 39) | public override long ReadInt64()
    method ReadUInt32 (line 47) | public override uint ReadUInt32()
    method ReadUInt64 (line 55) | public override ulong ReadUInt64()

FILE: src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs
  class BinaryWriter2 (line 6) | class BinaryWriter2 : BinaryWriter
    method BinaryWriter2 (line 9) | public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
    method WriteUInt (line 12) | public void WriteUInt(decimal n, int offset = 0)
    method Write (line 22) | public override void Write(string text)
    method WriteInt (line 27) | public void WriteInt(decimal n, int offset = 0)
    method WriteULong (line 37) | public void WriteULong(decimal n, int offset = 0)
    method WriteUShort (line 47) | public void WriteUShort(decimal n, int padding = 0)
    method WriteShort (line 60) | public void WriteShort(decimal n, int padding = 0)
    method WriteByte (line 73) | public void WriteByte(byte n, int padding = 0)

FILE: src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs
  class ParsedMP4Info (line 5) | public class ParsedMP4Info
  class MP4InitUtil (line 13) | public static class MP4InitUtil
    method ReadInit (line 18) | public static ParsedMP4Info ReadInit(byte[] data)
    method ReadBox (line 54) | private static void ReadBox(byte[] data, ParsedMP4Info info)

FILE: src/N_m3u8DL-RE.Parser/Mp4/MP4Parser.cs
  class ParsedBox (line 10) | class ParsedBox
  class TFHD (line 21) | class TFHD
  class TRUN (line 28) | class TRUN
  class Sample (line 34) | class Sample
  type BoxType (line 41) | enum BoxType
  class MP4Parser (line 47) | class MP4Parser
    method AllData (line 56) | public static BoxHandler AllData(DataHandler handler)
    method Children (line 65) | public static void Children(ParsedBox box)
    method SampleDescription (line 74) | public static void SampleDescription(ParsedBox box)
    method Parse (line 88) | public void Parse(byte[] data, bool partialOkay = false, bool stopOnPa...
    method ParseNext (line 98) | private void ParseNext(long absStart, BinaryReader2 reader, bool parti...
    method HeaderSize (line 192) | private static int HeaderSize(ParsedBox box)
    method TypeToString (line 199) | public static string TypeToString(long type)
    method TypeFromString (line 210) | private static int TypeFromString(string name)
    method Box (line 221) | public MP4Parser Box(string type, BoxHandler handler)
    method FullBox (line 229) | public MP4Parser FullBox(string type, BoxHandler handler)
    method ParseMDHD (line 237) | public static uint ParseMDHD(BinaryReader2 reader, uint version)
    method ParseTFDT (line 253) | public static ulong ParseTFDT(BinaryReader2 reader, uint version)
    method ParseTFHD (line 258) | public static TFHD ParseTFHD(BinaryReader2 reader, uint flags)
    method ParseTRUN (line 291) | public static TRUN ParseTRUN(BinaryReader2 reader, uint version, uint ...

FILE: src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs
  class SubEntity (line 8) | class SubEntity
    method Equals (line 16) | public override bool Equals(object? obj)
    method GetHashCode (line 25) | public override int GetHashCode()
  class MP4TtmlUtil (line 31) | public static partial class MP4TtmlUtil
    method AttrRegex (line 33) | [GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")]
    method LabelFixRegex (line 35) | [GeneratedRegex("<p.*?>((.|\n)+?)<\\/p>")]
    method MultiElementsFixRegex (line 37) | [GeneratedRegex(@"\<tt[\s\S]*?\<\/tt\>")]
    method ImageRegex (line 39) | [GeneratedRegex("\\<smpte:image.*xml:id=\\\"(.*?)\\\".*\\>([\\s\\S]*?)...
    method CheckInit (line 42) | public static bool CheckInit(byte[] data)
    method ShiftTime (line 62) | private static string ShiftTime(string xmlSrc, long segTimeMs, int index)
    method GetTextFromElement (line 108) | private static string GetTextFromElement(XmlElement node)
    method SplitMultipleRootElements (line 125) | private static List<string> SplitMultipleRootElements(string xml)
    method ExtractFromMp4 (line 130) | public static WebVttSub ExtractFromMp4(string item, long segTimeMs, lo...
    method ExtractFromMp4s (line 135) | private static WebVttSub ExtractFromMp4s(IEnumerable<string> items, lo...
    method ExtractFromTTML (line 173) | public static WebVttSub ExtractFromTTML(string item, long segTimeMs, l...
    method ExtractFromTTMLs (line 178) | public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, lo...
    method ExtractSub (line 193) | private static WebVttSub ExtractSub(List<string> xmls, long baseTimest...

FILE: src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs
  class MP4VttUtil (line 6) | public static class MP4VttUtil
    method CheckInit (line 8) | public static (bool, uint) CheckInit(byte[] data)
    method ExtractSub (line 36) | public static WebVttSub ExtractSub(IEnumerable<string> files, uint tim...
    method ParseVTTC (line 187) | private static SubCue? ParseVTTC(byte[] data, double startTime, double...

FILE: src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs
  class MSSMoovProcessor (line 14) | public partial class MSSMoovProcessor
    method KIDRegex (line 16) | [GeneratedRegex(@"\<KID\>(.*?)\<")]
    method MSSMoovProcessor (line 67) | public MSSMoovProcessor(StreamSpec streamSpec)
    method SamplingFrequencyIndex (line 98) | private int SamplingFrequencyIndex(int samplingRate) => samplingRate s...
    method GenCodecPrivateDataForAAC (line 116) | private void GenCodecPrivateDataForAAC()
    method ExtractKID (line 160) | private void ExtractKID()
    method CanHandle (line 184) | public static bool CanHandle(string fourCC) => SupportedFourCC.Contain...
    method Box (line 186) | private byte[] Box(string boxType, byte[] payload)
    method FullBox (line 198) | private byte[] FullBox(string boxType, byte version, uint flags, byte[...
    method GenSinf (line 210) | private byte[] GenSinf(string codec)
    method GenFtyp (line 241) | private byte[] GenFtyp()
    method GenMvhd (line 256) | private byte[] GenMvhd()
    method GenTkhd (line 286) | private byte[] GenTkhd()
    method GenMdhd (line 312) | private byte[] GenMdhd()
    method GenHdlr (line 327) | private byte[] GenHdlr()
    method GenMinf (line 346) | private byte[] GenMinf()
    method GenEsds (line 387) | private byte[] GenEsds(byte[] audioSpecificConfig)
    method GetSampleEntryBox (line 433) | private byte[] GetSampleEntryBox()
    method GetAvcC (line 576) | private byte[] GetAvcC(byte[] sps, byte[] pps)
    method GetHvcC (line 594) | private byte[] GetHvcC(byte[] sps, byte[] pps, byte[] vps, string code...
    method GetStsd (line 702) | private byte[] GetStsd()
    method GetMehd (line 714) | private byte[] GetMehd()
    method GetTrex (line 723) | private byte[] GetTrex()
    method GenPsshBoxForPlayReady (line 737) | private byte[] GenPsshBoxForPlayReady()
    method GenPsshBoxForWideVine (line 751) | private byte[] GenPsshBoxForWideVine()
    method GenMoof (line 766) | private byte[] GenMoof()
    method GenHeader (line 782) | public byte[] GenHeader(byte[] firstSegment)
    method GenHeader (line 796) | public byte[] GenHeader()

FILE: src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs
  class ContentProcessor (line 6) | public abstract class ContentProcessor
    method CanProcess (line 8) | public abstract bool CanProcess(ExtractorType extractorType, string ra...
    method Process (line 9) | public abstract string Process(string rawText, ParserConfig parserConf...

FILE: src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs
  class DefaultDASHContentProcessor (line 10) | public class DefaultDASHContentProcessor : ContentProcessor
    method CanProcess (line 19) | public override bool CanProcess(ExtractorType extractorType, string mp...
    method Process (line 26) | public override string Process(string mpdContent, ParserConfig parserC...
    method IsMissingNs (line 38) | private static bool IsMissingNs(string rawText, string tag)
    method ReplaceFirst (line 44) | private static string ReplaceFirst(string source, string oldValue, str...

FILE: src/N_m3u8DL-RE.Parser/Processor/DefaultUrlProcessor.cs
  class DefaultUrlProcessor (line 8) | public class DefaultUrlProcessor : UrlProcessor
    method CanProcess (line 10) | public override bool CanProcess(ExtractorType extractorType, string or...
    method Process (line 12) | public override string Process(string oriUrl, ParserConfig paserConfig)

FILE: src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs
  class DefaultHLSContentProcessor (line 8) | public partial class DefaultHLSContentProcessor : ContentProcessor
    method YkDVRegex (line 10) | [GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",...
    method DNSPRegex (line 12) | [GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONT...
    method DNSPSubRegex (line 14) | [GeneratedRegex(@"#EXTINF:.*?,\s+.*BUMPER.*\s+?#EXT-X-DISCONTINUITY")]
    method OrderFixRegex (line 16) | [GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
    method ATVRegex (line 18) | [GeneratedRegex(@"#EXT-X-MAP.*\.apple\.com/")]
    method ATVRegex2 (line 20) | [GeneratedRegex(@"(#EXT-X-KEY:[\s\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-EN...
    method CanProcess (line 23) | public override bool CanProcess(ExtractorType extractorType, string ra...
    method Process (line 25) | public override string Process(string m3u8Content, ParserConfig parser...

FILE: src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs
  class DefaultHLSKeyProcessor (line 12) | public class DefaultHLSKeyProcessor : KeyProcessor
    method CanProcess (line 14) | public override bool CanProcess(ExtractorType extractorType, string m3...
    method Process (line 17) | public override EncryptInfo Process(string keyLine, string m3u8Url, st...
    method PreProcessUrl (line 98) | private string PreProcessUrl(string url, ParserConfig parserConfig)

FILE: src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs
  class KeyProcessor (line 7) | public abstract class KeyProcessor
    method CanProcess (line 9) | public abstract bool CanProcess(ExtractorType extractorType, string ke...
    method Process (line 10) | public abstract EncryptInfo Process(string keyLine, string m3u8Url, st...

FILE: src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs
  class UrlProcessor (line 6) | public abstract class UrlProcessor
    method CanProcess (line 8) | public abstract bool CanProcess(ExtractorType extractorType, string or...
    method Process (line 9) | public abstract string Process(string oriUrl, ParserConfig parserConfig);

FILE: src/N_m3u8DL-RE.Parser/StreamExtractor.cs
  class StreamExtractor (line 13) | public class StreamExtractor
    method StreamExtractor (line 23) | public StreamExtractor(ParserConfig parserConfig)
    method LoadSourceFromUrlAsync (line 28) | public async Task LoadSourceFromUrlAsync(string url)
    method LoadSourceFromText (line 59) | [MemberNotNull(nameof(this.rawText), nameof(this.extractor))]
    method ExtractStreamsAsync (line 107) | public async Task<List<StreamSpec>> ExtractStreamsAsync()
    method FetchPlayListAsync (line 125) | public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
    method RefreshPlayListAsync (line 139) | public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)

FILE: src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs
  class ParserUtil (line 6) | public static partial class ParserUtil
    method VarsNumberRegex (line 8) | [GeneratedRegex(@"\$Number%([^$]+)d\$")]
    method GetAttribute (line 18) | public static string? GetAttribute(string line, string key = "")
    method GetRange (line 48) | public static (long, long?) GetRange(string input)
    method ParseRange (line 65) | public static (long, long) ParseRange(string range)
    method ReplaceVars (line 78) | public static string ReplaceVars(string text, Dictionary<string, objec...
    method CombineURL (line 103) | public static string CombineURL(string baseurl, string url)

FILE: src/N_m3u8DL-RE.Tests/Common/Util/HexUtilTests.cs
  class HexUtilTests (line 5) | public class HexUtilTests
    method BytesToHex_MultipleBytesWithDefaultSplit_ReturnsHexChars (line 7) | [Fact]
    method BytesToHex_MultipleBytesWithCustomSplit_ReturnsHexChars (line 14) | [Fact]

FILE: src/N_m3u8DL-RE.Tests/Parser/Extractor/DASHExtractor2Tests.cs
  class DASHExtractor2Tests (line 7) | public class DASHExtractor2Tests
    method CreateTestConfig (line 9) | private static ParserConfig CreateTestConfig(string mpdFileName) => ne...
    method DASHExtractor2_Normal (line 14) | [Fact]

FILE: src/N_m3u8DL-RE.Tests/ResourceHelper.cs
  class ResourceHelper (line 5) | public static class ResourceHelper
    method Read (line 10) | public static string Read(string fileName)

FILE: src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs
  class DownloadSpeedColumn (line 9) | internal sealed class DownloadSpeedColumn : ProgressColumn
    method DownloadSpeedColumn (line 16) | public DownloadSpeedColumn(ConcurrentDictionary<int, SpeedContainer> S...
    method Render (line 23) | public override IRenderable Render(RenderOptions options, ProgressTask...

FILE: src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs
  class DownloadStatusColumn (line 9) | internal class DownloadStatusColumn : ProgressColumn
    method DownloadStatusColumn (line 17) | public DownloadStatusColumn(ConcurrentDictionary<int, SpeedContainer> ...
    method Render (line 22) | public override IRenderable Render(RenderOptions options, ProgressTask...

FILE: src/N_m3u8DL-RE/Column/MyPercentageColumn.cs
  class MyPercentageColumn (line 6) | internal class MyPercentageColumn : ProgressColumn
    method Render (line 19) | public override IRenderable Render(RenderOptions options, ProgressTask...

FILE: src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs
  class RecordingDurationColumn (line 8) | internal class RecordingDurationColumn : ProgressColumn
    method RecordingDurationColumn (line 15) | public RecordingDurationColumn(ConcurrentDictionary<int, int> recoding...
    method RecordingDurationColumn (line 19) | public RecordingDurationColumn(ConcurrentDictionary<int, int> recoding...
    method Render (line 24) | public override IRenderable Render(RenderOptions options, ProgressTask...

FILE: src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs
  class RecordingSizeColumn (line 8) | internal class RecordingSizeColumn : ProgressColumn
    method RecordingSizeColumn (line 15) | public RecordingSizeColumn(ConcurrentDictionary<int, double> recodingS...
    method Render (line 19) | public override IRenderable Render(RenderOptions options, ProgressTask...

FILE: src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs
  class RecordingStatusColumn (line 6) | internal class RecordingStatusColumn : ProgressColumn
    method Render (line 11) | public override IRenderable Render(RenderOptions options, ProgressTask...

FILE: src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs
  class CommandInvoker (line 16) | internal static partial class CommandInvoker
    method ForStrRegex (line 20) | [GeneratedRegex("((best|worst)\\d*|all)")]
    method RangeRegex (line 22) | [GeneratedRegex(@"(\d*)-(\d*)")]
    method SpeedStrRegex (line 24) | [GeneratedRegex(@"([\d\\.]+)(M|K)")]
    method PairKeyRegex (line 26) | [GeneratedRegex("^[0-9a-fA-f]{32}:[0-9a-fA-f]{32}$")]
    method IdHexKeyRegex (line 28) | [GeneratedRegex("^[0-9]{1,}:[0-9a-fA-f]{32}$")]
    method SingleHexKeyRegex (line 30) | [GeneratedRegex("^[0-9a-fA-f]{32}$")]
    method ParseSpeedLimit (line 124) | private static long? ParseSpeedLimit(ArgumentResult result)
    method ParseCustomRange (line 150) | private static CustomRange? ParseCustomRange(ArgumentResult result)
    method ParseProxy (line 200) | private static WebProxy? ParseProxy(ArgumentResult result)
    method ParseCustomKeys (line 232) | private static string[]? ParseCustomKeys(ArgumentResult result)
    method ParseHLSCustomKey (line 295) | private static byte[]? ParseHLSCustomKey(ArgumentResult result)
    method ParseLiveLimit (line 320) | private static TimeSpan? ParseLiveLimit(ArgumentResult result)
    method ParseStartTime (line 339) | private static DateTime? ParseStartTime(ArgumentResult result)
    method ParseSaveName (line 354) | private static string? ParseSaveName(ArgumentResult result)
    method ParseFilePath (line 366) | private static string? ParseFilePath(ArgumentResult result)
    method ParseStreamFilter (line 395) | private static StreamFilter? ParseStreamFilter(ArgumentResult result)
    method ParseHeaders (line 491) | private static Dictionary<string, string> ParseHeaders(ArgumentResult ...
    method ParseImports (line 502) | private static List<OutputFile> ParseImports(ArgumentResult result)
    method ParseMuxAfterDone (line 534) | private static MuxOptions? ParseMuxAfterDone(ArgumentResult result)
    method HasOption (line 590) | private static bool HasOption(this ParseResult result, Option option)
    method WithDefault (line 597) | private static Option<T> WithDefault<T>(this Option<T> option, T defau...
    method GetOptions (line 611) | private static MyOption GetOptions(ParseResult result)
    method InvokeArgs (line 705) | public static async Task<int> InvokeArgs(string[] args, Func<MyOption,...

FILE: src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs
  class ComplexParamParser (line 5) | internal class ComplexParamParser
    method ComplexParamParser (line 8) | public ComplexParamParser(string arg)
    method GetValue (line 13) | public string? GetValue(string key)

FILE: src/N_m3u8DL-RE/CommandLine/MyOption.cs
  class MyOption (line 9) | internal class MyOption

FILE: src/N_m3u8DL-RE/Config/DownloaderConfig.cs
  class DownloaderConfig (line 5) | internal class DownloaderConfig

FILE: src/N_m3u8DL-RE/Config/EnvConfigKey.cs
  class EnvConfigKey (line 6) | public static class EnvConfigKey

FILE: src/N_m3u8DL-RE/Crypto/AESUtil.cs
  class AESUtil (line 5) | internal static class AESUtil
    method AES128Decrypt (line 15) | public static void AES128Decrypt(string filePath, byte[] keyByte, byte...
    method AES128Decrypt (line 22) | public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByt...

FILE: src/N_m3u8DL-RE/Crypto/CSChaCha20.cs
  class ChaCha20 (line 29) | public sealed class ChaCha20 : IDisposable
    method ChaCha20 (line 73) | public ChaCha20(byte[] key, byte[] nonce, uint counter)
    method ChaCha20 (line 90) | public ChaCha20(ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce, uint...
    method KeySetup (line 121) | private void KeySetup(byte[] key)
    method IvSetup (line 161) | private void IvSetup(byte[] nonce, uint counter)
    method EncryptBytes (line 193) | public void EncryptBytes(byte[] output, byte[] input, int numBytes)
    method EncryptStream (line 204) | public void EncryptStream(Stream output, Stream input, int howManyByte...
    method EncryptStreamAsync (line 215) | public async Task EncryptStreamAsync(Stream output, Stream input, int ...
    method EncryptBytes (line 226) | public void EncryptBytes(byte[] output, byte[] input)
    method EncryptBytes (line 238) | public byte[] EncryptBytes(byte[] input, int numBytes)
    method EncryptBytes (line 251) | public byte[] EncryptBytes(byte[] input)
    method EncryptString (line 264) | public byte[] EncryptString(string input)
    method DecryptBytes (line 285) | public void DecryptBytes(byte[] output, byte[] input, int numBytes)
    method DecryptStream (line 296) | public void DecryptStream(Stream output, Stream input, int howManyByte...
    method DecryptStreamAsync (line 307) | public async Task DecryptStreamAsync(Stream output, Stream input, int ...
    method DecryptBytes (line 318) | public void DecryptBytes(byte[] output, byte[] input)
    method DecryptBytes (line 330) | public byte[] DecryptBytes(byte[] input, int numBytes)
    method DecryptBytes (line 343) | public byte[] DecryptBytes(byte[] input)
    method DecryptUTF8ByteArray (line 356) | public string DecryptUTF8ByteArray(byte[] input)
    method WorkStreams (line 366) | private void WorkStreams(Stream output, Stream input, int howManyBytes...
    method WorkStreamsAsync (line 383) | private async Task WorkStreamsAsync(Stream output, Stream input, int h...
    method WorkBytes (line 408) | private void WorkBytes(byte[] output, byte[] input, int numBytes)
    method QuarterRound (line 502) | private static void QuarterRound(uint[] x, uint a, uint b, uint c, uin...
    method Dispose (line 530) | public void Dispose()
    method Dispose (line 545) | private void Dispose(bool disposing)
  class Util (line 567) | public static class Util
    method Rotate (line 575) | [MethodImpl(MethodImplOptions.AggressiveInlining)]
    method XOr (line 590) | [MethodImpl(MethodImplOptions.AggressiveInlining)]
    method Add (line 605) | [MethodImpl(MethodImplOptions.AggressiveInlining)]
    method AddOne (line 619) | [MethodImpl(MethodImplOptions.AggressiveInlining)]
    method U8To32Little (line 631) | [MethodImpl(MethodImplOptions.AggressiveInlining)]
    method ToBytes (line 649) | [MethodImpl(MethodImplOptions.AggressiveInlining)]

FILE: src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs
  class ChaCha20Util (line 5) | internal static class ChaCha20Util
    method DecryptPer1024Bytes (line 7) | public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] ...

FILE: src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs
  class HTTPLiveRecordManager (line 17) | internal class HTTPLiveRecordManager
    method HTTPLiveRecordManager (line 35) | public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<S...
    method RecordStreamAsync (line 45) | private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, Prog...
    method ReadInfoAsync (line 115) | public async Task ReadInfoAsync()
    method TimeCounterAsync (line 179) | public async Task TimeCounterAsync()
    method StartRecordAsync (line 196) | public async Task<bool> StartRecordAsync()

FILE: src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs
  class SimpleDownloadManager (line 20) | internal class SimpleDownloadManager
    method SimpleDownloadManager (line 28) | public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<S...
    method SearchKeyAsync (line 37) | private async Task SearchKeyAsync(string? currentKID)
    method ChangeSpecInfo (line 49) | private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> med...
    method DownloadStreamAsync (line 80) | private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, Pr...
    method StartDownloadAsync (line 646) | public async Task<bool> StartDownloadAsync()

FILE: src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs
  class SimpleLiveRecordManager2 (line 23) | internal class SimpleLiveRecordManager2
    method SimpleLiveRecordManager2 (line 47) | public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, Lis...
    method SearchKeyAsync (line 57) | private async Task SearchKeyAsync(string? currentKID)
    method GetUnixTimestamp (line 74) | private long GetUnixTimestamp(DateTime dateTime)
    method GetSegmentName (line 85) | private string GetSegmentName(MediaSegment segment, bool allHasDatetim...
    method ChangeSpecInfo (line 112) | private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> med...
    method RecordStreamAsync (line 144) | private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, Prog...
    method PlayListProduceAsync (line 654) | private async Task PlayListProduceAsync(Dictionary<StreamSpec, Progres...
    method FilterMediaSegments (line 733) | private void FilterMediaSegments(StreamSpec streamSpec, ProgressTask t...
    method StartRecordAsync (line 773) | public async Task<bool> StartRecordAsync()

FILE: src/N_m3u8DL-RE/Downloader/IDownloader.cs
  type IDownloader (line 6) | internal interface IDownloader
    method DownloadSegmentAsync (line 8) | Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, strin...

FILE: src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs
  class SimpleDownloader (line 15) | internal class SimpleDownloader : IDownloader
    method SimpleDownloader (line 19) | public SimpleDownloader(DownloaderConfig config)
    method DownloadSegmentAsync (line 24) | public async Task<DownloadResult?> DownloadSegmentAsync(MediaSegment s...
    method DownClipAsync (line 79) | private async Task<(string des, DownloadResult? dResult)> DownClipAsyn...

FILE: src/N_m3u8DL-RE/Entity/CustomRange.cs
  class CustomRange (line 3) | public class CustomRange
    method ToString (line 12) | public override string? ToString()

FILE: src/N_m3u8DL-RE/Entity/DownloadResult.cs
  class DownloadResult (line 3) | internal class DownloadResult

FILE: src/N_m3u8DL-RE/Entity/Mediainfo.cs
  class Mediainfo (line 5) | internal class Mediainfo
    method ToString (line 18) | public override string? ToString()
    method ToStringMarkUp (line 23) | public string ToStringMarkUp()

FILE: src/N_m3u8DL-RE/Entity/MuxOptions.cs
  class MuxOptions (line 5) | internal class MuxOptions

FILE: src/N_m3u8DL-RE/Entity/OutputFile.cs
  class OutputFile (line 5) | internal class OutputFile

FILE: src/N_m3u8DL-RE/Entity/SpeedContainer.cs
  class SpeedContainer (line 3) | internal class SpeedContainer
    method AddLowSpeedCount (line 20) | public int AddLowSpeedCount()
    method ResetLowSpeedCount (line 25) | public int ResetLowSpeedCount()
    method Add (line 30) | public long Add(long size)
    method Reset (line 36) | public void Reset()
    method ResetVars (line 41) | public void ResetVars()

FILE: src/N_m3u8DL-RE/Entity/StreamFilter.cs
  class StreamFilter (line 7) | public class StreamFilter
    method ToString (line 28) | public override string? ToString()

FILE: src/N_m3u8DL-RE/Enum/DecryptEngine.cs
  type DecryptEngine (line 3) | internal enum DecryptEngine

FILE: src/N_m3u8DL-RE/Enum/MuxFormat.cs
  type MuxFormat (line 3) | internal enum MuxFormat

FILE: src/N_m3u8DL-RE/Enum/SubtitleFormat.cs
  type SubtitleFormat (line 3) | internal enum SubtitleFormat

FILE: src/N_m3u8DL-RE/Processor/DemoProcessor.cs
  class DemoProcessor (line 8) | internal class DemoProcessor : ContentProcessor
    method CanProcess (line 11) | public override bool CanProcess(ExtractorType extractorType, string ra...
    method Process (line 16) | public override string Process(string rawText, ParserConfig parserConfig)

FILE: src/N_m3u8DL-RE/Processor/DemoProcessor2.cs
  class DemoProcessor2 (line 11) | internal class DemoProcessor2 : KeyProcessor
    method CanProcess (line 13) | public override bool CanProcess(ExtractorType extractorType, string ke...
    method Process (line 18) | public override EncryptInfo Process(string keyLine, string m3u8Url, st...

FILE: src/N_m3u8DL-RE/Processor/NowehoryzontyUrlProcessor.cs
  class NowehoryzontyUrlProcessor (line 14) | internal class NowehoryzontyUrlProcessor : UrlProcessor
    method CanProcess (line 23) | public override bool CanProcess(ExtractorType extractorType, string or...
    method Process (line 44) | public override string Process(string oriUrl, ParserConfig parserConfig)
    method Calc (line 50) | private static string Calc(string path)

FILE: src/N_m3u8DL-RE/Program.cs
  class Program (line 21) | internal class Program
    method Main (line 23) | static async Task Main(string[] args)
    method Console_CancelKeyPress (line 56) | private static void Console_CancelKeyPress(object? sender, ConsoleCanc...
    method GetOrder (line 68) | static int GetOrder(StreamSpec streamSpec)
    method DoWorkAsync (line 76) | static async Task DoWorkAsync(MyOption option)
    method WriteRawFilesAsync (line 419) | private static async Task WriteRawFilesAsync(MyOption option, StreamEx...
    method CheckUpdateAsync (line 434) | static async Task CheckUpdateAsync()
    method Get302Async (line 455) | static async Task<string> Get302Async(string url)

FILE: src/N_m3u8DL-RE/Util/CultureUtil.cs
  class CultureUtil (line 5) | public static class CultureUtil
    method GetCurrentCultureName (line 7) | public static string GetCurrentCultureName()
    method ChangeCurrentCultureName (line 23) | public static void ChangeCurrentCultureName(string newName)
    method GetCurrentCultureNameFromEnvironment (line 37) | private static string GetCurrentCultureNameFromEnvironment()

FILE: src/N_m3u8DL-RE/Util/DownloadUtil.cs
  class DownloadUtil (line 9) | internal static class DownloadUtil
    method CopyFileAsync (line 13) | private static async Task<DownloadResult> CopyFileAsync(string sourceF...
    method DownloadToFileAsync (line 38) | public static async Task<DownloadResult> DownloadToFileAsync(string ur...

FILE: src/N_m3u8DL-RE/Util/FilterUtil.cs
  class FilterUtil (line 11) | public static class FilterUtil
    method DoFilterKeep (line 13) | public static List<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> li...
    method DoFilterDrop (line 66) | public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> li...
    method SelectStreams (line 78) | public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> l...
    method SyncStreams (line 147) | public static void SyncStreams(List<StreamSpec> selectedSteams, int ta...
    method ApplyCustomRange (line 198) | public static void ApplyCustomRange(List<StreamSpec> selectedSteams, C...
    method CleanAd (line 240) | public static void CleanAd(List<StreamSpec> selectedSteams, string[]? ...

FILE: src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs
  class ImageHeaderUtil (line 3) | internal static class ImageHeaderUtil
    method IsImageHeader (line 5) | public static bool IsImageHeader(byte[] bArr)
    method ProcessAsync (line 23) | public static async Task ProcessAsync(string sourcePath)

FILE: src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs
  class Language (line 5) | internal class Language(string extendCode, string code, string desc, str...
  class LanguageCodeUtil (line 13) | internal static class LanguageCodeUtil
    method ConvertTwoToThree (line 492) | private static string ConvertTwoToThree(string input)
    method ConvertLangCodeAndDisplayName (line 502) | public static void ConvertLangCodeAndDisplayName(OutputFile outputFile)

FILE: src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs
  class LargeSingleFileSplitUtil (line 7) | internal static class LargeSingleFileSplitUtil
    class Clip (line 9) | class Clip
    method SplitUrlAsync (line 22) | public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegme...
    method CanSplitAsync (line 49) | public static async Task<bool> CanSplitAsync(string url, Dictionary<st...
    method GetFileSizeAsync (line 66) | private static async Task<long> GetFileSizeAsync(string url, Dictionar...
    method GetAllClips (line 82) | private static List<Clip> GetAllClips(long fileSize)

FILE: src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs
  class MP4DecryptUtil (line 10) | internal static partial class MP4DecryptUtil
    method DecryptAsync (line 13) | public static async Task<bool> DecryptAsync(DecryptEngine decryptEngin...
    method RunCommandAsync (line 126) | private static async Task<bool> RunCommandAsync(string name, string ar...
    method SearchKeyFromFileAsync (line 150) | public static async Task<string?> SearchKeyFromFileAsync(string? file,...
    method GetMP4Info (line 175) | public static ParsedMP4Info GetMP4Info(byte[] data)
    method GetMP4Info (line 184) | public static ParsedMP4Info GetMP4Info(string output)
    method ReadInitShaka (line 192) | public static string? ReadInitShaka(string output, string bin)
    method KidOutputRegex (line 217) | [GeneratedRegex("Key for key_id=([0-9a-f]+) was not found")]

FILE: src/N_m3u8DL-RE/Util/MediainfoUtil.cs
  class MediainfoUtil (line 7) | internal static partial class MediainfoUtil
    method TextRegex (line 9) | [GeneratedRegex("  Stream #.*")]
    method IdRegex (line 11) | [GeneratedRegex(@"#0:\d(\[0x\w+?\])")]
    method TypeRegex (line 13) | [GeneratedRegex(": (\\w+): (.*)")]
    method BaseInfoRegex (line 15) | [GeneratedRegex("(.*?)(,|$)")]
    method ReplaceRegex (line 17) | [GeneratedRegex(@" \/ 0x\w+")]
    method ResRegex (line 19) | [GeneratedRegex(@"\d{2,}x\d+")]
    method BitrateRegex (line 21) | [GeneratedRegex(@"\d+ kb\/s")]
    method FpsRegex (line 23) | [GeneratedRegex(@"(\d+(\.\d+)?) fps")]
    method DoViRegex (line 25) | [GeneratedRegex(@"DOVI configuration record.*profile: (\d).*compatibil...
    method StartRegex (line 27) | [GeneratedRegex(@"Duration.*?start: (\d+\.?\d{0,3})")]
    method ReadInfoAsync (line 30) | public static async Task<List<Mediainfo>> ReadInfoAsync(string binary,...

FILE: src/N_m3u8DL-RE/Util/MergeUtil.cs
  class MergeUtil (line 10) | internal static class MergeUtil
    method CombineMultipleFilesIntoSingleFile (line 17) | public static void CombineMultipleFilesIntoSingleFile(string[] files, ...
    method InvokeFFmpeg (line 41) | private static int InvokeFFmpeg(string binary, string command, string ...
    method PartialCombineMultipleFiles (line 68) | public static string[] PartialCombineMultipleFiles(string[] files)
    method MergeByFFmpeg (line 96) | public static bool MergeByFFmpeg(string binary, string[] files, string...
    method MuxInputsByFFmpeg (line 174) | public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files...
    method MuxInputsByMkvmerge (line 252) | public static bool MuxInputsByMkvmerge(string binary, OutputFile[] fil...

FILE: src/N_m3u8DL-RE/Util/OtherUtil.cs
  class OtherUtil (line 7) | internal static partial class OtherUtil
    method SplitHeaderArrayToDic (line 9) | public static Dictionary<string, string> SplitHeaderArrayToDic(string[...
    method GetValidFileName (line 28) | public static string GetValidFileName(string input, string re = "_", b...
    method GetFileNameFromInput (line 45) | public static string GetFileNameFromInput(string input, bool addSuffix...
    method ParseDur (line 66) | public static TimeSpan ParseDur(string timeStr)
    method ParseSeconds (line 95) | public static double ParseSeconds(string timeStr)
    method SafeDeleteDir (line 114) | public static void SafeDeleteDir(string dirPath)
    method DeGzipFileAsync (line 135) | public static async Task DeGzipFileAsync(string filePath)
    method GetEnvironmentVariable (line 155) | public static string GetEnvironmentVariable(string key, string default...
    method GetMuxExtension (line 160) | public static string GetMuxExtension(MuxFormat muxFormat)
    method TimeStrRegex (line 171) | [GeneratedRegex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$")]
    method FormatSavePattern (line 182) | public static string FormatSavePattern(string savePattern, Common.Enti...
    method HandleFileCollision (line 212) | public static string HandleFileCollision(string originalPath, Common.E...

FILE: src/N_m3u8DL-RE/Util/PipeUtil.cs
  class PipeUtil (line 10) | internal static class PipeUtil
    method CreatePipe (line 12) | public static Stream CreatePipe(string pipeName)
    method StartPipeMuxAsync (line 36) | public static async Task<bool> StartPipeMuxAsync(string binary, string...
    method StartPipeMux (line 45) | public static bool StartPipeMux(string binary, string[] pipeNames, str...

FILE: src/N_m3u8DL-RE/Util/SubtitleUtil.cs
  class SubtitleUtil (line 7) | internal static class SubtitleUtil
    method TryWriteImagePngsAsync (line 15) | public static async Task TryWriteImagePngsAsync(WebVttSub? finalVtt, s...
Condensed preview — 114 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (661K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 838,
    "preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
  },
  {
    "path": ".github/workflows/build_latest.yml",
    "chars": 13365,
    "preview": "name: Build Latest\n\non:\n  workflow_dispatch:\n    inputs:\n      doRelease:\n        description: 'Publish new release'\n   "
  },
  {
    "path": ".gitignore",
    "chars": 6282,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": "LICENSE",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2022 nilaoda\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "README.en.md",
    "chars": 15268,
    "preview": "# N_m3u8DL-RE [EN]\n\nCross-platform DASH/HLS/MSS download tool. Supports on-demand and live streaming (DASH/HLS).\n\n[![img"
  },
  {
    "path": "README.md",
    "chars": 12098,
    "preview": "# N_m3u8DL-RE\n\n[See English version here](README.en.md)\n\n跨平台的DASH/HLS/MSS下载工具。支持点播、直播(DASH/HLS)。\n\n[![img](https://img.sh"
  },
  {
    "path": "TestStreams.md",
    "chars": 2196,
    "preview": "# Test Streams\n\n* https://play.itunes.apple.com/WebObjects/MZPlay.woa/hls/subscription/playlist.m3u8?cc=US&svcId=tvs.vds"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs",
    "chars": 2012,
    "preview": "using N_m3u8DL_RE.Entity;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing System.Collections.Concurrent;\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs",
    "chars": 1759,
    "preview": "using N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Entity;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing "
  },
  {
    "path": "src/N_m3u8DL-RE/Column/MyPercentageColumn.cs",
    "chars": 819,
    "preview": "using Spectre.Console.Rendering;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal class MyPercentageColu"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs",
    "chars": 1305,
    "preview": "using N_m3u8DL_RE.Common.Util;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing System.Collections.Concurr"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs",
    "chars": 1301,
    "preview": "using N_m3u8DL_RE.Common.Util;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing System.Collections.Concurr"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs",
    "chars": 691,
    "preview": "using Spectre.Console;\nusing Spectre.Console.Rendering;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal class RecordingStatusC"
  },
  {
    "path": "src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs",
    "chars": 35748,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common"
  },
  {
    "path": "src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs",
    "chars": 1583,
    "preview": "using System.Text;\n\nnamespace N_m3u8DL_RE.CommandLine;\n\ninternal class ComplexParamParser\n{\n    private readonly string"
  },
  {
    "path": "src/N_m3u8DL-RE/CommandLine/MyOption.cs",
    "chars": 9675,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Entity;\nusing N_m3u8DL_RE.Enum;\nusing Sy"
  },
  {
    "path": "src/N_m3u8DL-RE/Config/DownloaderConfig.cs",
    "chars": 620,
    "preview": "using N_m3u8DL_RE.CommandLine;\n\nnamespace N_m3u8DL_RE.Config;\n\ninternal class DownloaderConfig\n{\n    public required My"
  },
  {
    "path": "src/N_m3u8DL-RE/Config/EnvConfigKey.cs",
    "chars": 571,
    "preview": "namespace N_m3u8DL_RE.Config;\n\n/// <summary>\n/// 通过配置环境变量来实现更细节地控制某些逻辑\n/// </summary>\npublic static class EnvConfigKey\n{"
  },
  {
    "path": "src/N_m3u8DL-RE/Crypto/AESUtil.cs",
    "chars": 1316,
    "preview": "using System.Security.Cryptography;\n\nnamespace N_m3u8DL_RE.Crypto;\n\ninternal static class AESUtil\n{\n    /// <summary>\n "
  },
  {
    "path": "src/N_m3u8DL-RE/Crypto/CSChaCha20.cs",
    "chars": 27761,
    "preview": "/*\n * Copyright (c) 2015, 2018 Scott Bennett\n *           (c) 2018-2021 Kaarlo Räihä\n *\n * Permission to use, copy, mod"
  },
  {
    "path": "src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs",
    "chars": 1296,
    "preview": "using CSChaCha20;\n\nnamespace N_m3u8DL_RE.Crypto;\n\ninternal static class ChaCha20Util\n{\n    public static byte[] Decrypt"
  },
  {
    "path": "src/N_m3u8DL-RE/Directory.Build.props",
    "chars": 1698,
    "preview": "<Project>\n\n    <PropertyGroup>\n      <IlcOptimizationPreference>Speed</IlcOptimizationPreference>\n      <IlcFoldIdentica"
  },
  {
    "path": "src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs",
    "chars": 10631,
    "preview": "using N_m3u8DL_RE.Column;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resou"
  },
  {
    "path": "src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs",
    "chars": 35856,
    "preview": "using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Column;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusin"
  },
  {
    "path": "src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs",
    "chars": 43647,
    "preview": "using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Column;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusin"
  },
  {
    "path": "src/N_m3u8DL-RE/Downloader/IDownloader.cs",
    "chars": 292,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Entity;\n\nnamespace N_m3u8DL_RE.Downloader;\n\ninternal interface IDown"
  },
  {
    "path": "src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs",
    "chars": 5830,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Config;\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/CustomRange.cs",
    "chars": 450,
    "preview": "namespace N_m3u8DL_RE.Entity;\n\npublic class CustomRange\n{\n    public required string InputStr { get; set; }\n    public "
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/DownloadResult.cs",
    "chars": 504,
    "preview": "namespace N_m3u8DL_RE.Entity;\n\ninternal class DownloadResult\n{\n    public bool Success => (ActualContentLength != null "
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/Mediainfo.cs",
    "chars": 943,
    "preview": "using Spectre.Console;\n\nnamespace N_m3u8DL_RE.Entity;\n\ninternal class Mediainfo\n{\n    public string? Id { get; set; }\n "
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/MuxOptions.cs",
    "chars": 341,
    "preview": "using N_m3u8DL_RE.Enum;\n\nnamespace N_m3u8DL_RE.Entity;\n\ninternal class MuxOptions\n{\n    public bool UseMkvmerge { get; "
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/OutputFile.cs",
    "chars": 378,
    "preview": "using N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Entity;\n\ninternal class OutputFile\n{\n    public MediaType? MediaT"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/SpeedContainer.cs",
    "chars": 1262,
    "preview": "namespace N_m3u8DL_RE.Entity;\n\ninternal class SpeedContainer\n{\n    public bool SingleSegment { get; set; } = false;\n   "
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/StreamFilter.cs",
    "chars": 2282,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Entity;\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Enum/DecryptEngine.cs",
    "chars": 108,
    "preview": "namespace N_m3u8DL_RE.Enum;\n\ninternal enum DecryptEngine\n{\n    MP4DECRYPT,\n    SHAKA_PACKAGER,\n    FFMPEG,\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Enum/MuxFormat.cs",
    "chars": 82,
    "preview": "namespace N_m3u8DL_RE.Enum;\n\ninternal enum MuxFormat\n{\n    MP4,\n    MKV,\n    TS,\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Enum/SubtitleFormat.cs",
    "chars": 79,
    "preview": "namespace N_m3u8DL_RE.Enum;\n\ninternal enum SubtitleFormat\n{\n    VTT,\n    SRT\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/N_m3u8DL-RE.csproj",
    "chars": 847,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</Tar"
  },
  {
    "path": "src/N_m3u8DL-RE/Processor/DemoProcessor.cs",
    "chars": 605,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser."
  },
  {
    "path": "src/N_m3u8DL-RE/Processor/DemoProcessor2.cs",
    "chars": 1003,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common."
  },
  {
    "path": "src/N_m3u8DL-RE/Processor/NowehoryzontyUrlProcessor.cs",
    "chars": 2406,
    "preview": "using System.Security.Cryptography;\nusing System.Text;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusi"
  },
  {
    "path": "src/N_m3u8DL-RE/Program.cs",
    "chars": 17460,
    "preview": "using System.Globalization;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common."
  },
  {
    "path": "src/N_m3u8DL-RE/Util/CultureUtil.cs",
    "chars": 1482,
    "preview": "using N_m3u8DL_RE.Common.Resource;\n\nnamespace N_m3u8DL_RE.Util;\n\npublic static class CultureUtil\n{\n    public static str"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/DownloadUtil.cs",
    "chars": 5993,
    "preview": "using N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Entit"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/FilterUtil.cs",
    "chars": 11387,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common."
  },
  {
    "path": "src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs",
    "chars": 3576,
    "preview": "namespace N_m3u8DL_RE.Util;\n\ninternal static class ImageHeaderUtil\n{\n    public static bool IsImageHeader(byte[] bArr)\n "
  },
  {
    "path": "src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs",
    "chars": 17588,
    "preview": "using N_m3u8DL_RE.Entity;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal class Language(string extendCode, string code, string "
  },
  {
    "path": "src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs",
    "chars": 3520,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Util;\n\nnamespace N_m3u8DL_RE.Ut"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs",
    "chars": 7714,
    "preview": "using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing System.Diagnostics;\nusi"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/MediainfoUtil.cs",
    "chars": 3279,
    "preview": "using N_m3u8DL_RE.Entity;\nusing System.Diagnostics;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Util;\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/MergeUtil.cs",
    "chars": 11521,
    "preview": "using N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Entity;\nusing Spectre.Console;\nusing System.Diagnostics;\nusing System.T"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/OtherUtil.cs",
    "chars": 10108,
    "preview": "using N_m3u8DL_RE.Enum;\nusing System.IO.Compression;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Util;"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/PipeUtil.cs",
    "chars": 3380,
    "preview": "using N_m3u8DL_RE.Common.Log;\nusing Spectre.Console;\nusing System.Diagnostics;\nusing System.IO.Pipes;\nusing System.Text"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/SubtitleUtil.cs",
    "chars": 1081,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\n\nnamespace N_m3u8DL_R"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/EncryptInfo.cs",
    "chars": 778,
    "preview": "using N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Common.Entity;\n\npublic class EncryptInfo\n{\n    /// <summary>\n    "
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/MSSData.cs",
    "chars": 618,
    "preview": "namespace N_m3u8DL_RE.Common.Entity;\n\npublic class MSSData\n{\n    public string FourCC { get; set; } = \"\";\n    public st"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/MediaPart.cs",
    "chars": 157,
    "preview": "namespace N_m3u8DL_RE.Common.Entity;\n\n// 主要处理 EXT-X-DISCONTINUITY\npublic class MediaPart\n{\n    public List<MediaSegment"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs",
    "chars": 1300,
    "preview": "using N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Common.Entity;\n\npublic class MediaSegment\n{\n    public long Index"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/Playlist.cs",
    "chars": 580,
    "preview": "namespace N_m3u8DL_RE.Common.Entity;\n\npublic class Playlist\n{\n    // 对应Url信息\n    public string Url { get; set; } = stri"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs",
    "chars": 6098,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Util;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Common.Enti"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/SubCue.cs",
    "chars": 642,
    "preview": "namespace N_m3u8DL_RE.Common.Entity;\n\npublic class SubCue\n{\n    public TimeSpan StartTime { get; set; }\n    public Time"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs",
    "chars": 8477,
    "preview": "using System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Common.Entity;\n\npublic partial class We"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/Choise.cs",
    "chars": 83,
    "preview": "namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum Choise\n{\n    YES = 1,\n    NO = 0\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/EncryptMethod.cs",
    "chars": 178,
    "preview": "namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum EncryptMethod\n{\n    NONE,\n    AES_128,\n    AES_128_ECB,\n    SAMPLE_AES,"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs",
    "chars": 113,
    "preview": "namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum ExtractorType\n{\n    MPEG_DASH,\n    HLS,\n    HTTP_LIVE,\n    MSS\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/MediaType.cs",
    "chars": 135,
    "preview": "namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum MediaType\n{\n    AUDIO = 0,\n    VIDEO = 1,\n    SUBTITLES = 2,\n    CLOSED"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/RoleType.cs",
    "chars": 244,
    "preview": "namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum RoleType\n{\n    Subtitle = 0,\n    Main = 1,\n    Alternate = 2,\n    Suppl"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/JsonContext/JsonContext.cs",
    "chars": 826,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing System.Text.Json.Serialization;\n\nnamespace N_m3u8"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/JsonConverter/BytesBase64Converter.cs",
    "chars": 473,
    "preview": "using System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace N_m3u8DL_RE.Common.JsonConverter;\n\ninternal cl"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Log/CustomAnsiConsole.cs",
    "chars": 2810,
    "preview": "using System.Text;\nusing System.Text.RegularExpressions;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Common.Log;\n\npub"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Log/LogLevel.cs",
    "chars": 111,
    "preview": "namespace N_m3u8DL_RE.Common.Log;\n\npublic enum LogLevel\n{\n    OFF,\n    ERROR,\n    WARN,\n    INFO,\n    DEBUG,\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Log/Logger.cs",
    "chars": 6791,
    "preview": "using Spectre.Console;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Common.Log;\n\npub"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/N_m3u8DL-RE.Common.csproj",
    "chars": 442,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n\t<OutputType>library</OutputType>\n    <TargetFramework>net10.0</Ta"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Resource/ResString.cs",
    "chars": 10413,
    "preview": "namespace N_m3u8DL_RE.Common.Resource;\n\npublic static class ResString\n{\n    public static string CurrentLoc { get; set;"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Resource/StaticText.cs",
    "chars": 41872,
    "preview": "namespace N_m3u8DL_RE.Common.Resource;\n\ninternal static class StaticText\n{\n    public static readonly Dictionary<string"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Resource/TextContainer.cs",
    "chars": 315,
    "preview": "namespace N_m3u8DL_RE.Common.Resource;\n\ninternal class TextContainer\n{\n    public string ZH_CN { get; }\n    public stri"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/BinaryContentCheckUtil.cs",
    "chars": 2178,
    "preview": "namespace N_m3u8DL_RE.Common.Util;\n\npublic static class BinaryContentCheckUtil\n{\n    public static bool LooksLikeBinary("
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs",
    "chars": 2663,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.JsonConverter;\nusing System.Text.Json;\nusing System.Text.Json"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs",
    "chars": 6695,
    "preview": "using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Com"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/HexUtil.cs",
    "chars": 1488,
    "preview": "namespace N_m3u8DL_RE.Common.Util;\n\npublic static class HexUtil\n{\n    public static string BytesToHex(byte[] data, strin"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/RetryUtil.cs",
    "chars": 1212,
    "preview": "using System.Net;\nusing N_m3u8DL_RE.Common.Log;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Common.Util;\n\npublic stati"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs",
    "chars": 2030,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Processor;\nusing N_m3u8DL_RE.Parser.Processor.DASH;\nusing N_m3u"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs",
    "chars": 310,
    "preview": "namespace N_m3u8DL_RE.Parser.Constants;\n\ninternal static class DASHTags\n{\n    public const string TemplateRepresentatio"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Constants/HLSTags.cs",
    "chars": 1728,
    "preview": "namespace N_m3u8DL_RE.Parser.Constants;\n\ninternal static class HLSTags\n{\n    public const string ext_m3u = \"#EXTM3U\";\n "
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs",
    "chars": 279,
    "preview": "namespace N_m3u8DL_RE.Parser.Constants;\n\ninternal static class MSSTags\n{\n    public const string Bitrate = \"{Bitrate}\";"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs",
    "chars": 32567,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Parser."
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs",
    "chars": 20786,
    "preview": "using N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Comm"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs",
    "chars": 513,
    "preview": "using N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/LiveTSExtractor.cs",
    "chars": 1279,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Pa"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs",
    "chars": 16073,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common."
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/InternalsVisibleTo.cs",
    "chars": 90,
    "preview": "using System.Runtime.CompilerServices;\n[assembly: InternalsVisibleTo(\"N_m3u8DL-RE.Tests\")]"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/BinaryReader2.cs",
    "chars": 1471,
    "preview": "namespace Mp4SubtitleParser;\n\n// make BinaryReader in Big Endian\nclass BinaryReader2 : BinaryReader\n{\n    public Binary"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs",
    "chars": 2122,
    "preview": "using System.Text;\n\nnamespace Mp4SubtitleParser;\n\n// make BinaryWriter in Big Endian\nclass BinaryWriter2 : BinaryWriter"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs",
    "chars": 3550,
    "preview": "using N_m3u8DL_RE.Common.Util;\n\nnamespace Mp4SubtitleParser\n{\n    public class ParsedMP4Info\n    {\n        public strin"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4Parser.cs",
    "chars": 11161,
    "preview": "using System.Text;\n\n/**\n * Translated from shaka-player project\n * https://github.com/nilaoda/Mp4SubtitleParser\n * http"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs",
    "chars": 13739,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Xml;\n\nnamespace "
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs",
    "chars": 8128,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing System.Text;\n\nnamespace Mp4SubtitleParser;\n\npublic static class MP4VttUtil\n{\n   "
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs",
    "chars": 34670,
    "preview": "using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Util;\nusing System.Text;\nusing Syste"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/N_m3u8DL-RE.Parser.csproj",
    "chars": 447,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>library</OutputType>\n    <TargetFramework>net10.0</"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs",
    "chars": 337,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Parser.Processor;\n\npublic abstra"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs",
    "chars": 1837,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Pa"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/DefaultUrlProcessor.cs",
    "chars": 1291,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Parser.Config;\nusing System.Web;\n\nnamesp"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs",
    "chars": 3790,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Constants;\nusing System.Text.R"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs",
    "chars": 3695,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common."
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs",
    "chars": 443,
    "preview": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs",
    "chars": 331,
    "preview": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Parser.Processor;\n\npublic abstra"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/StreamExtractor.cs",
    "chars": 4735,
    "preview": "using System.Diagnostics.CodeAnalysis;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs",
    "chars": 3650,
    "preview": "using N_m3u8DL_RE.Parser.Constants;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Parser.Util;\n\npublic s"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/Common/Util/HexUtilTests.cs",
    "chars": 508,
    "preview": "using N_m3u8DL_RE.Common.Util;\n\nnamespace N_m3u8DL_RE.Tests.Common.Util;\n\npublic class HexUtilTests\n{\n    [Fact]\n    pub"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/N_m3u8DL-RE.Tests.csproj",
    "chars": 1146,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFramework>net10.0</TargetFramework>\n        <RootN"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/Parser/Extractor/DASHExtractor2Tests.cs",
    "chars": 1369,
    "preview": "using Shouldly;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Extractor;\n\nnamespace N_m3u8DL_RE.Tests.Parser"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/ResourceHelper.cs",
    "chars": 663,
    "preview": "using System.Reflection;\n\nnamespace N_m3u8DL_RE.Tests;\n\npublic static class ResourceHelper\n{\n    private static readonly"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/Resources/Dash/Manifest_1080p.mpd",
    "chars": 9298,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nVersion information:\nAxinom.MediaProcessing v3.0.0 targeting General Purpos"
  },
  {
    "path": "src/N_m3u8DL-RE.sln",
    "chars": 2655,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.3.3250"
  }
]

About this extraction

This page contains the full source code of the nilaoda/N_m3u8DL-RE GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 114 files (614.5 KB), approximately 160.4k tokens, and a symbol index with 522 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!