[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: ['https://nilaoda.github.io/N_m3u8DL-CLI/source/images/alipay.png','https://www.buymeacoffee.com/nilaoda']\n"
  },
  {
    "path": ".github/workflows/build_latest.yml",
    "content": "name: Build Latest\n\non:\n  workflow_dispatch:\n    inputs:\n      doRelease:\n        description: 'Publish new release'\n        type: boolean\n        default: false\n        required: false\n      tag:\n        type: string\n        description: 'Release version tag (e.g. v0.2.1-beta)'\n        required: true\n      ref:\n        type: string\n        description: 'Git ref from which to release'\n        required: true\n        default: 'main'\n\nenv:\n  DOTNET_SDK_VERSION: \"10.0.101\"\n  ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n\njobs:\n  set-date:\n    runs-on: ubuntu-latest\n    outputs:\n      date: ${{ steps.get_date.outputs.date }}\n      tag: ${{ steps.format_tag.outputs.tag }}\n    steps:\n      - name: Get Date in UTC+8\n        id: get_date\n        run: |\n          DATE=$(date -u -d '8 hours' +'%Y%m%d')\n          echo \"date=${DATE}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Determine Tag\n        id: format_tag\n        run: |\n          if [ \"${{ github.event.inputs.doRelease }}\" == \"true\" ]; then\n            TAG=\"${{ github.event.inputs.tag }}\"\n          else\n            TAG=\"actions-$GITHUB_RUN_ID\"\n          fi\n          echo \"tag=${TAG}\" >> \"$GITHUB_OUTPUT\"\n\n  build-win-nt6_0-x86:\n    runs-on: windows-latest\n    needs: set-date\n\n    steps:\n      - uses: actions/checkout@v1\n        with:\n          ref: ${{ github.event.inputs.ref }}\n\n      - name: Install zip\n        run: choco install zip --no-progress --yes\n\n      - name: Set up dotnet\n        uses: actions/setup-dotnet@v3\n        with:\n          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}\n\n      - 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\"\n      - run: dotnet add src/N_m3u8DL-RE/N_m3u8DL-RE.csproj package YY-Thunks --version 1.1.4\n      - run: dotnet add src/N_m3u8DL-RE/N_m3u8DL-RE.csproj package VC-LTL --version 5.1.1\n      - run: dotnet publish src/N_m3u8DL-RE -p:TargetPlatformMinVersion=6.0 -r win-x86 -c Release -o artifact-x86\n\n      - name: Package [win-x86]\n        run: |\n          cd artifact-x86\n          zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-NT6.0-x86_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe\n\n      - name: Upload Artifact[win-x86]\n        uses: actions/upload-artifact@v4\n        with:\n          name: win-NT6.0-x86\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-NT6.0-x86_${{ needs.set-date.outputs.date }}.zip\n\n  build-win-x64-arm64:\n    runs-on: windows-latest\n    needs: set-date\n\n    steps:\n      - uses: actions/checkout@v1\n        with:\n          ref: ${{ github.event.inputs.ref }}\n\n      - name: Install zip\n        run: choco install zip --no-progress --yes\n\n      - name: Set up dotnet\n        uses: actions/setup-dotnet@v3\n        with:\n          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}\n\n      - run: dotnet publish src/N_m3u8DL-RE -r win-x64 -c Release -o artifact-x64\n      - run: dotnet publish src/N_m3u8DL-RE -r win-arm64 -c Release -o artifact-arm64\n\n      - name: Package [win]\n        run: |\n          cd artifact-x64\n          zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-x64_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe\n          cd ../artifact-arm64\n          zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-arm64_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe\n\n      - name: Upload Artifact [win-x64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: win-x64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-x64_${{ needs.set-date.outputs.date }}.zip\n\n      - name: Upload Artifact [win-arm64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: win-arm64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-arm64_${{ needs.set-date.outputs.date }}.zip\n\n  build-android-bionic-x64-arm64:\n    runs-on: windows-latest\n    needs: set-date\n\n    steps:\n      - uses: actions/checkout@v1\n        with:\n          ref: ${{ github.event.inputs.ref }}\n\n      - name: Set up NDK\n        shell: pwsh\n        run: |\n          Invoke-WebRequest -Uri \"https://dl.google.com/android/repository/android-ndk-r27c-windows.zip\" -OutFile \"android-ndk.zip\"\n          Expand-Archive -Path \"android-ndk.zip\" -DestinationPath \"./android-ndk\"\n          Get-ChildItem -Path \"./android-ndk\"\n          $ndkRoot = \"${{ github.workspace }}\\android-ndk\\android-ndk-r27c\"\n          echo \"NDK_ROOT=$ndkRoot\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8\n          $ndkBinPath = \"$ndkRoot\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\"\n          echo $ndkBinPath | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8\n\n      - name: Set up dotnet\n        uses: actions/setup-dotnet@v3\n        with:\n          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}\n\n      - run: dotnet publish src/N_m3u8DL-RE -r linux-bionic-x64 -p:DisableUnsupportedError=true -p:PublishAotUsingRuntimePack=true -o artifact\n      - run: dotnet publish src/N_m3u8DL-RE -r linux-bionic-arm64 -p:DisableUnsupportedError=true -p:PublishAotUsingRuntimePack=true -o artifact-arm64\n\n      - name: Package [linux-bionic]\n        run: |\n          cd artifact\n          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE\n          cd ../artifact-arm64\n          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE\n\n      - name: Upload Artifact [linux-bionic-x64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: android-bionic-x64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-x64_${{ needs.set-date.outputs.date }}.tar.gz\n\n      - name: Upload Artifact[linux-bionic-arm64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: android-bionic-arm64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-arm64_${{ needs.set-date.outputs.date }}.tar.gz\n\n  build-linux-x64:\n    runs-on: ubuntu-latest\n    needs: set-date\n\n    steps:\n      - uses: actions/checkout@v1\n        with:\n          ref: ${{ github.event.inputs.ref }}\n\n      - name: Build x64 in alpine container\n        run: |\n          # 在 alpine 容器中执行完整构建流程\n          docker run --rm \\\n            -v \"$PWD:/workspace\" \\\n            -w /workspace \\\n            alpine:3.21 \\\n            sh -c \"\n              set -e\n          \n              # 安装编译和运行依赖\n              apk add --no-cache bash wget tar clang build-base cmake icu-dev icu-data-full zlib-static openssl-dev openssl-libs-static\n  \n              # 下载并安装 .NET SDK\n              DOTNET_SDK_VERSION='${{ env.DOTNET_SDK_VERSION }}'\n              DOTNET_SDK_URL=\\\"https://builds.dotnet.microsoft.com/dotnet/Sdk/\\${DOTNET_SDK_VERSION}/dotnet-sdk-\\${DOTNET_SDK_VERSION}-linux-musl-x64.tar.gz\\\"\n              wget -nv \\\"\\$DOTNET_SDK_URL\\\" -O dotnet-sdk.tar.gz\n              mkdir -p /opt/dotnet\n              tar -xzf dotnet-sdk.tar.gz -C /opt/dotnet\n              export PATH=\\\"/opt/dotnet:\\$PATH\\\"\n          \n              # 编译 Native AOT 输出到挂载的 artifact 目录\n              dotnet publish src/N_m3u8DL-RE -r linux-musl-x64 -c Release -o /workspace/artifact\n            \"\n\n      - name: Package [linux-x64]\n        run: |\n          cd artifact\n          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE\n\n      - name: Upload Artifact [linux-x64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux-x64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-x64_${{ needs.set-date.outputs.date }}.tar.gz\n\n  build-linux-arm64:\n    runs-on: ubuntu-latest\n    needs: set-date\n\n    steps:\n      - uses: actions/checkout@v1\n        with:\n          ref: ${{ github.event.inputs.ref }}\n\n      - name: Build arm64 in alpine container\n        run: |\n          # 在交叉编译环境容器中执行完整构建流程\n          docker run --rm \\\n            -v \"$PWD:/workspace\" \\\n            -w /workspace \\\n            mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-cross-arm64-alpine \\\n            bash -c \"\n              set -e\n              \n              # 确保 Ubuntu 宿主环境有所有依赖\n              apt-get update && apt-get install -y build-essential clang binutils-aarch64-linux-gnu\n          \n              # 动态获取 Alpine 最新 OpenSSL 版本号\n              ALPINE_REPO='https://dl-cdn.alpinelinux.org/alpine/v3.21/main/aarch64'\n              echo \\\"Fetching latest OpenSSL version from Alpine...\\\"\n              # 自动抓取 openssl-dev 的完整文件名并解析版本号\n              SSL_APK_NAME=\\$(wget -qO- \\$ALPINE_REPO/ | grep -o 'openssl-dev-[0-9.]\\+-r[0-9]\\+.apk' | head -n 1)\n              SSL_VER=\\$(echo \\$SSL_APK_NAME | sed 's/openssl-dev-//;s/.apk//')\n              echo \\\"Detected OpenSSL Version: \\$SSL_VER\\\"\n              \n              # 下载并提取 ARM64 的 OpenSSL 静态库到 SysRoot\n              mkdir -p /tmp/ssl_setup && cd /tmp/ssl_setup\n              wget -q \\$ALPINE_REPO/openssl-libs-static-\\$SSL_VER.apk\n              wget -q \\$ALPINE_REPO/openssl-dev-\\$SSL_VER.apk\n              \n              # 解压到交叉编译系统的根目录\n              for f in *.apk; do tar -xf \\$f -C /crossrootfs/arm64; done\n              cd /workspace\n              \n              # 设置交叉编译环境变量 (强制 CMake 和编译器使用正确路径)\n              export CROSS_COMPILE_PREFIX=\\\"aarch64-alpine-linux-musl-\\\"\n              export CC=\\\"\\${CROSS_COMPILE_PREFIX}clang\\\"\n              export CXX=\\\"\\${CROSS_COMPILE_PREFIX}clang++\\\"\n              \n              # 告知 .NET 本地脚本去哪里找 OpenSSL 静态库\n              export OPENSSL_ROOT_DIR=\\\"/crossrootfs/arm64/usr\\\"\n              export OPENSSL_USE_STATIC_LIBS=TRUE\n              \n              # 额外保险：设置 CFLAGS 让 CMake 内部探测更准确\n              export CFLAGS=\\\"--sysroot=/crossrootfs/arm64 --target=aarch64-alpine-linux-musl\\\"\n          \n              # 下载并安装 .NET SDK\n              DOTNET_SDK_VERSION='${{ env.DOTNET_SDK_VERSION }}'\n              DOTNET_SDK_URL=\\\"https://builds.dotnet.microsoft.com/dotnet/Sdk/\\${DOTNET_SDK_VERSION}/dotnet-sdk-\\${DOTNET_SDK_VERSION}-linux-x64.tar.gz\\\"\n              wget -nv \\\"\\$DOTNET_SDK_URL\\\" -O dotnet-sdk.tar.gz\n              mkdir -p /opt/dotnet\n              tar -xzf dotnet-sdk.tar.gz -C /opt/dotnet\n              export PATH=\\\"/opt/dotnet:\\$PATH\\\"\n          \n              # 交叉编译\n              dotnet publish src/N_m3u8DL-RE -r linux-musl-arm64 -c Release \\\n                -p:SysRoot=/crossrootfs/arm64 \\\n                -p:CppCompilerAndLinker=clang \\\n                -p:StripSymbols=true \\\n                -o /workspace/artifact\n            \"\n\n      - name: Package [linux-arm64]\n        run: |\n          cd artifact\n          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE\n\n      - name: Upload Artifact [linux-x64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: linux-arm64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-arm64_${{ needs.set-date.outputs.date }}.tar.gz\n\n  build-mac-x64-arm64:\n    runs-on: macos-latest\n    needs: set-date\n\n    steps:\n      - uses: actions/checkout@v1\n        with:\n          ref: ${{ github.event.inputs.ref }}\n\n      - name: Set up dotnet\n        uses: actions/setup-dotnet@v3\n        with:\n          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}\n      - run: dotnet publish src/N_m3u8DL-RE -r osx-arm64 -c Release -o artifact-arm64 \n      - run: dotnet publish src/N_m3u8DL-RE -r osx-x64 -c Release -o artifact-x64 \n\n      - name: Package [osx]\n        run: |\n          cd artifact-x64\n          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE\n          cd ../artifact-arm64\n          tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE\n\n      - name: Upload Artifact [osx-x64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: osx-x64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-x64_${{ needs.set-date.outputs.date }}.tar.gz\n\n      - name: Upload Artifact[osx-arm64]\n        uses: actions/upload-artifact@v4\n        with:\n          name: osx-arm64\n          path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-arm64_${{ needs.set-date.outputs.date }}.tar.gz\n\n  create_release:\n    name: Create release\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    if: ${{ github.event.inputs.doRelease == 'true' }}\n    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]\n\n    steps:\n      - name: Fetch artifacts\n        uses: actions/download-artifact@v4\n\n      - name: Create GitHub Release\n        uses: ncipollo/release-action@v1\n        with:\n          tag: ${{ github.event.inputs.tag }}\n          name: N_m3u8DL-RE_${{ github.event.inputs.tag }}\n          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/*\"\n          draft: false\n          allowUpdates: true\n          generateReleaseNotes: true\n          discussionCategory: 'Announcements'"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Properties\nProperties/\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n#[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Rider\n.idea\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# macOS shit\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 nilaoda\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.en.md",
    "content": "# N_m3u8DL-RE [EN]\n\nCross-platform DASH/HLS/MSS download tool. Supports on-demand and live streaming (DASH/HLS).\n\n[![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)\n\nIf 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.\n\n---\n\nThe 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).\n\nArch 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)\n\n```bash\n# Install N_m3u8DL-RE release version on Arch Linux and its derivatives (not maintained by the author)\nyay -Syu n-m3u8dl-re-bin\n\n# Install N_m3u8DL-RE development version on Arch Linux and its derivatives (not maintained by the author)\nyay -Syu n-m3u8dl-re-git\n```\n\n---\n\n## Command line parameters\n\n```\nDescription:\n  N_m3u8DL-RE (Beta version) 20241203\n\nUsage:\n  N_m3u8DL-RE <input> [options]\n\nArguments:\n  <input>  Input Url or File\n\nOptions:\n  --tmp-dir <tmp-dir>                                     Set temporary file directory\n  --save-dir <save-dir>                                   Set output directory\n  --save-name <save-name>                                 Set output filename\n  --base-url <base-url>                                   Set BaseURL\n  --thread-count <number>                                 Set download thread count [default: based on the number of CPU cores]\n  --download-retry-count <number>                         The number of retries when download segment error [default: 3]\n  --http-request-timeout <seconds>                        Timeout duration for HTTP requests (in seconds) [default: 100]\n  --force-ansi-console                                    Force assuming the terminal is ANSI-compatible and interactive\n  --no-ansi-color                                         Remove ANSI colors\n  --auto-select                                           Automatically selects the best tracks of all types [default:\n                                                          False]\n  --skip-merge                                            Skip segments merge [default: False]\n  --skip-download                                         Skip download [default: False]\n  --check-segments-count                                  Check if the actual number of segments downloaded matches the\n                                                          expected number [default: True]\n  --binary-merge                                          Binary merge [default: False]\n  --use-ffmpeg-concat-demuxer                             When merging with ffmpeg, use the concat demuxer instead of\n                                                          the concat protocol [default: False]\n  --del-after-done                                        Delete temporary files when done [default: True]\n  --no-date-info                                          Date information is not written during muxing [default: False]\n  --no-log                                                Disable log file output [default: False]\n  --write-meta-json                                       Write meta json after parsed [default: True]\n  --append-url-params                                     Add Params of input Url to segments, useful for some\n                                                          websites, such as kakao.com [default: False]\n  -mt, --concurrent-download                              Concurrently download the selected audio, video and subtitles\n                                                          [default: False]\n  -H, --header <header>                                   Pass custom header(s) to server, Example:\n                                                          -H \"Cookie: mycookie\" -H \"User-Agent: iOS\"\n  --sub-only                                              Select only subtitle tracks [default: False]\n  --sub-format <SRT|VTT>                                  Subtitle output format [default: SRT]\n  --auto-subtitle-fix                                     Automatically fix subtitles [default: True]\n  --ffmpeg-binary-path <PATH>                             Full path to the ffmpeg binary, like C:\\Tools\\ffmpeg.exe\n  --log-level <DEBUG|ERROR|INFO|OFF|WARN>                 Set log level [default: INFO]\n  --ui-language <en-US|zh-CN|zh-TW>                       Set UI language\n  --urlprocessor-args <urlprocessor-args>                 Give these arguments to the URL Processors.\n  --key <key>                                             Set decryption key(s) to mp4decrypt/shaka-packager/ffmpeg.\n                                                          format:\n                                                          --key KID1:KEY1 --key KID2:KEY2\n                                                          or use --key KEY if all tracks share the same key.\n  --key-text-file <key-text-file>                         Set the kid-key file, the program will search the KEY with\n                                                          KID from the file.(Very large file are not recommended)\n  --decryption-engine <FFMPEG|MP4DECRYPT|SHAKA_PACKAGER>  Set the third-party program used for decryption [default:\n                                                          MP4DECRYPT]\n  --decryption-binary-path <PATH>                         Full path to the tool used for MP4 decryption, like\n                                                          C:\\Tools\\mp4decrypt.exe\n  --mp4-real-time-decryption                              Decrypt MP4 segments in real time [default: False]\n  -R, --max-speed <SPEED>                                 Set speed limit, Mbps or Kbps, for example: 15M 100K.\n  -M, --mux-after-done <OPTIONS>                          When all works is done, try to mux the downloaded streams.\n                                                          Use \"--morehelp mux-after-done\" for more details\n  --custom-hls-method <METHOD>                            Set HLS encryption method\n                                                          (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_\n                                                          CTR|UNKNOWN)\n  --custom-hls-key <FILE|HEX|BASE64>                      Set the HLS decryption key. Can be file, HEX or Base64\n  --custom-hls-iv <FILE|HEX|BASE64>                       Set the HLS decryption iv. Can be file, HEX or Base64\n  --use-system-proxy                                      Use system default proxy [default: True]\n  --custom-proxy <URL>                                    Set web request proxy, like http://127.0.0.1:8888\n  --custom-range <RANGE>                                  Download only part of the segments. Use \"--morehelp\n                                                          custom-range\" for more details\n  --task-start-at <yyyyMMddHHmmss>                        Task execution will not start before this time\n  --live-perform-as-vod                                   Download live streams as vod [default: False]\n  --live-real-time-merge                                  Real-time merge into file when recording live [default: False]\n  --live-keep-segments                                    Keep segments when recording a live (liveRealTimeMerge\n                                                          enabled) [default: True]\n  --live-pipe-mux                                         Real-time muxing to TS file through pipeline + ffmpeg\n                                                          (liveRealTimeMerge enabled) [default: False]\n  --live-fix-vtt-by-audio                                 Correct VTT sub by reading the start time of the audio file\n                                                          [default: False]\n  --live-record-limit <HH:mm:ss>                          Recording time limit when recording live\n  --live-wait-time <SEC>                                  Manually set the live playlist refresh interval\n  --live-take-count <NUM>                                 Manually set the number of segments downloaded for the first\n                                                          time when recording live [default: 16]\n  --mux-import <OPTIONS>                                  When MuxAfterDone enabled, allow to import local media files.\n                                                          Use \"--morehelp mux-import\" for more details\n  -sv, --select-video <OPTIONS>                           Select video streams by regular expressions. Use \"--morehelp\n                                                          select-video\" for more details\n  -sa, --select-audio <OPTIONS>                           Select audio streams by regular expressions. Use \"--morehelp\n                                                          select-audio\" for more details\n  -ss, --select-subtitle <OPTIONS>                        Select subtitle streams by regular expressions. Use\n                                                          \"--morehelp select-subtitle\" for more details\n  -dv, --drop-video <OPTIONS>                             Drop video streams by regular expressions.\n  -da, --drop-audio <OPTIONS>                             Drop audio streams by regular expressions.\n  -ds, --drop-subtitle <OPTIONS>                          Drop subtitle streams by regular expressions.\n  --ad-keyword <REG>                                      Set URL keywords (regular expressions) for AD segments\n  --disable-update-check                                  Disable version update check [default: False]\n  --allow-hls-multi-ext-map                               Allow multiple #EXT-X-MAP in HLS (experimental) [default:\n                                                          False]\n  --morehelp <OPTION>                                     Set more help info about one option\n  --version                                               Show version information\n  -?, -h, --help                                          Show help and usage information\n```\n\n<details>\n<summary>Click to view \"More Help\" section</summary>\n\n```\nMore Help:\n\n  --mux-after-done\n\n所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\n\n* format=FORMAT: 指定混流容器 mkv, mp4\n* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\n* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\n* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\n* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)\n\n例如:\n# 混流为mp4容器\n-M format=mp4\n# 使用mkvmerge, 自动寻找程序\n-M format=mkv:muxer=mkvmerge\n# 使用mkvmerge, 自定义程序路径\n-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\n```\n\n```\nMore Help:\n\n  --mux-import\n\nWhen MuxAfterDone enabled, allow to import local media files. OPTIONS is a colon separated list of:\n\n* path=PATH: set file path\n* lang=CODE: set media language code (not required)\n* name=NAME: set description (not required)\n\nExamples:\n# import subtitle\n--mux-import path=en-US.srt:lang=eng:name=\"English (Original)\"\n# import audio and subtitle\n--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)\"\n```\n\n```\nMore Help:\n\n  --select-video\n\nSelect video streams by regular expressions. OPTIONS is a colon separated list of:\n\nid=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\nsegsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\nplistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\n\n* for=FOR: Select type. best[number], worst[number], all (Default: best)\n\nExamples:\n# select best video\n-sv best\n# select 4K+HEVC video\n-sv res=\"3840*\":codecs=hvc1:for=best\n# Select best video with duration longer than 1 hour 20 minutes 30 seconds\n-sv plistDurMin=\"1h20m30s\":for=best\n-sv role=\"main\":for=best\n# Select video with bandwidth between 800Kbps and 1Mbps\n-sv bwMin=800:bwMax=1000\n```\n\n```\nMore Help:\n\n  --select-audio\n\nSelect audio streams by regular expressions. ref --select-video\n\nExamples:\n# select all\n-sa all\n# select best eng audio\n-sa lang=en:for=best\n# select best 2, and language is ja or en\n-sa lang=\"ja|en\":for=best2\n-sa role=\"main\":for=best\n```\n\n```\nMore Help:\n\n  --select-subtitle\n\nSelect subtitle streams by regular expressions. ref --select-video\n\nExamples:\n# select all subs\n-ss all\n# select all subs containing \"English\"\n-ss name=\"English\":for=all\n```\n\n```\nMore Help:\n\n  --custom-range\n\nDownload only part of the segments when downloading vod content.\n\nExamples:\n# Download [0,10], a total of 11 segments\n--custom-range 0-10\n# Download subsequent segments starting from index 10\n--custom-range 10-\n# Download the first 100 segments\n--custom-range -99\n# Download content from the 05:00 to 20:00\n--custom-range 05:00-20:00\n```\n\n</details>\n\n## Screenshots\n\n### On-demand\n\n![RE1](img/RE.gif)\n\nCan also download in parallel and automatically mix streams\n\n![RE2](img/RE2.gif)\n\n### Live\n\nRecord TS live source:\n\n[click to show gif](http://pan.iqiyi.com/file/paopao/W0LfmaMRvuA--uCdOpZ1cldM5JCVhMfIm7KFqr4oKCz80jLn0bBb-9PWmeCFZ-qHpAaQydQ1zk-CHYT_UbRLtw.gif)\n\nRecord MPD live source:\n\n[click to show gif](http://pan.iqiyi.com/file/paopao/nmAV5MOh0yIyHhnxdgM_6th_p2nqrFsM4k-o3cUPwUa8Eh8QOU4uyPkLa_BlBrMa3GBnKWSk8rOaUwbsjKN14g.gif)\n\nDuring recording, use ffmpeg to mix audio and video in real time\n\n```bash\nffmpeg -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\n```\n\nFrom v0.1.5, you can try to enable `live-pipe-mux` instead of the above command\n\n> [!NOTE]\n> 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.\n\nFrom 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>\n\n## Donate\n\n<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>\n"
  },
  {
    "path": "README.md",
    "content": "# N_m3u8DL-RE\n\n[See English version here](README.en.md)\n\n跨平台的DASH/HLS/MSS下载工具。支持点播、直播(DASH/HLS)。\n\n[![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)\n\n遇到 BUG 请首先确认软件是否为最新版本（如果是 Release 版本，建议到 [Actions](https://github.com/nilaoda/N_m3u8DL-RE/actions) 页面下载最新自动构建版本后查看问题是否已经被修复），如果确认版本最新且问题依旧存在，可以到 [Issues](https://github.com/nilaoda/N_m3u8DL-RE/issues) 中查找是否有人遇到过相关问题，没有的话再进行询问。\n\n---\n\n版本较低的Windows系统自带的终端可能不支持本程序，替代方案：在 [cmder](https://github.com/cmderdev/cmder) 中运行。\n\nArch 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)\n\n```bash\n# Arch Linux 及其衍生版安装 N_m3u8DL-RE 发行版 (该源非本人维护)\nyay -Syu n-m3u8dl-re-bin\n\n# Arch Linux 及其衍生版安装 N_m3u8DL-RE 开发版 (该源非本人维护)\nyay -Syu n-m3u8dl-re-git\n```\n\n---\n\n## 命令行参数\n\n```\nDescription:\n  N_m3u8DL-RE (Beta version) 20251027\n\nUsage:\n  N_m3u8DL-RE <input> [options]\n\nArguments:\n  <input>  链接或文件\n\nOptions:\n  --tmp-dir <tmp-dir>                                     设置临时文件存储目录\n  --save-dir <save-dir>                                   设置输出目录\n  --save-name <save-name>                                 设置保存文件名\n  --save-pattern <save-pattern>                           设置保存文件命名模板, 支持使用变量: \n                                                          <SaveName>, <Id>, <Codecs>, <Language>, <Resolution>, \n                                                          <Bandwidth>, <MediaType>, <Channels>, <FrameRate>, \n                                                          <VideoRange>, <GroupId>, <Ext>\n                                                          示例: --save-pattern \"<SaveName>_<Resolution>_<Bandwidth>\"\n  --log-file-path <log-file-path>                         设置日志文件路径, 例如 C:\\Logs\\log.txt\n  --base-url <base-url>                                   设置BaseURL\n  --thread-count <number>                                 设置下载线程数 [default: 本机CPU线程数]\n  --download-retry-count <number>                         每个分片下载异常时的重试次数 [default: 3]\n  --http-request-timeout <seconds>                        HTTP请求的超时时间(秒) [default: 100]\n  --force-ansi-console                                    强制认定终端为支持ANSI且可交互的终端\n  --no-ansi-color                                         去除ANSI颜色\n  --auto-select                                           自动选择所有类型的最佳轨道 [default: False]\n  --skip-merge                                            跳过合并分片 [default: False]\n  --skip-download                                         跳过下载 [default: False]\n  --check-segments-count                                  检测实际下载的分片数量和预期数量是否匹配 [default: True]\n  --binary-merge                                          二进制合并 [default: False]\n  --use-ffmpeg-concat-demuxer                             使用 ffmpeg 合并时，使用 concat 分离器而非 concat 协议 [default: False]\n  --del-after-done                                        完成后删除临时文件 [default: True]\n  --no-date-info                                          混流时不写入日期信息 [default: False]\n  --no-log                                                关闭日志文件输出 [default: False]\n  --write-meta-json                                       解析后的信息是否输出json文件 [default: True]\n  --append-url-params                                     将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com [default: False]\n  -mt, --concurrent-download                              并发下载已选择的音频、视频和字幕 [default: False]\n  -H, --header <header>                                   为HTTP请求设置特定的请求头, 例如:\n                                                          -H \"Cookie: mycookie\" -H \"User-Agent: iOS\"\n  --sub-only                                              只选取字幕轨道 [default: False]\n  --sub-format <SRT|VTT>                                  字幕输出类型 [default: SRT]\n  --auto-subtitle-fix                                     自动修正字幕 [default: True]\n  --ffmpeg-binary-path <PATH>                             ffmpeg可执行程序全路径, 例如 C:\\Tools\\ffmpeg.exe\n  --log-level <DEBUG|ERROR|INFO|OFF|WARN>                 设置日志级别 [default: INFO]\n  --ui-language <en-US|zh-CN|zh-TW>                       设置UI语言\n  --urlprocessor-args <urlprocessor-args>                 此字符串将直接传递给URL Processor\n  --key <key>                                             设置解密密钥, 程序调用mp4decrpyt/shaka-packager/ffmpeg进行解密. 格式:\n                                                          --key KID1:KEY1 --key KID2:KEY2\n                                                          对于KEY相同的情况可以直接输入 --key KEY\n  --key-text-file <key-text-file>                         设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)\n  --decryption-engine <FFMPEG|MP4DECRYPT|SHAKA_PACKAGER>  设置解密时使用的第三方程序 [default: MP4DECRYPT]\n  --decryption-binary-path <PATH>                         MP4解密所用工具的全路径, 例如 C:\\Tools\\mp4decrypt.exe\n  --mp4-real-time-decryption                              实时解密MP4分片 [default: False]\n  -R, --max-speed <SPEED>                                 设置限速，单位支持 Mbps 或 Kbps，如：15M 100K\n  -M, --mux-after-done <OPTIONS>                          所有工作完成时尝试混流分离的音视频. 输入 \"--morehelp mux-after-done\" 以查看详细信息\n  --custom-hls-method <METHOD>                            指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)\n  --custom-hls-key <FILE|HEX|BASE64>                      指定HLS解密KEY. 可以是文件, HEX或Base64\n  --custom-hls-iv <FILE|HEX|BASE64>                       指定HLS解密IV. 可以是文件, HEX或Base64\n  --use-system-proxy                                      使用系统默认代理 [default: True]\n  --custom-proxy <URL>                                    设置请求代理, 如 http://127.0.0.1:8888\n  --custom-range <RANGE>                                  仅下载部分分片. 输入 \"--morehelp custom-range\" 以查看详细信息\n  --task-start-at <yyyyMMddHHmmss>                        在此时间之前不会开始执行任务\n  --live-perform-as-vod                                   以点播方式下载直播流 [default: False]\n  --live-real-time-merge                                  录制直播时实时合并 [default: False]\n  --live-keep-segments                                    录制直播并开启实时合并时依然保留分片 [default: True]\n  --live-pipe-mux                                         录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件 [default: False]\n  --live-fix-vtt-by-audio                                 通过读取音频文件的起始时间修正VTT字幕 [default: False]\n  --live-record-limit <HH:mm:ss>                          录制直播时的录制时长限制\n  --live-wait-time <SEC>                                  手动设置直播列表刷新间隔\n  --live-take-count <NUM>                                 手动设置录制直播时首次获取分片的数量 [default: 16]\n  --mux-import <OPTIONS>                                  混流时引入外部媒体文件. 输入 \"--morehelp mux-import\" 以查看详细信息\n  -sv, --select-video <OPTIONS>                           通过正则表达式选择符合要求的视频流. 输入 \"--morehelp select-video\" 以查看详细信息\n  -sa, --select-audio <OPTIONS>                           通过正则表达式选择符合要求的音频流. 输入 \"--morehelp select-audio\" 以查看详细信息\n  -ss, --select-subtitle <OPTIONS>                        通过正则表达式选择符合要求的字幕流. 输入 \"--morehelp select-subtitle\" 以查看详细信息\n  -dv, --drop-video <OPTIONS>                             通过正则表达式去除符合要求的视频流.\n  -da, --drop-audio <OPTIONS>                             通过正则表达式去除符合要求的音频流.\n  -ds, --drop-subtitle <OPTIONS>                          通过正则表达式去除符合要求的字幕流.\n  --ad-keyword <REG>                                      设置广告分片的URL关键字(正则表达式)\n  --disable-update-check                                  禁用版本更新检测 [default: False]\n  --allow-hls-multi-ext-map                               允许HLS中的多个#EXT-X-MAP(实验性) [default: False]\n  --morehelp <OPTION>                                     查看某个选项的详细帮助信息\n  -?, -h, --help                                          Show help and usage information\n  --version                                               Show version information\n```\n\n<details>\n<summary>点击查看More Help</summary>\n\n```\nMore Help:\n\n  --mux-after-done\n\n所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\n\n* format=FORMAT: 指定混流容器 mkv, mp4\n* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\n* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\n* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\n* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)\n\n例如:\n# 混流为mp4容器\n-M format=mp4\n# 使用mkvmerge, 自动寻找程序\n-M format=mkv:muxer=mkvmerge\n# 使用mkvmerge, 自定义程序路径\n-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\n```\n\n```\nMore Help:\n\n  --mux-import\n\n混流时引入外部媒体文件. 你能够以:分隔形式指定如下参数:\n\n* path=PATH: 指定媒体文件路径\n* lang=CODE: 指定媒体文件语言代码 (非必须)\n* name=NAME: 指定媒体文件描述信息 (非必须)\n\n例如:\n# 引入外部字幕\n--mux-import path=zh-Hans.srt:lang=chi:name=\"中文 (简体)\"\n# 引入外部音轨+字幕\n--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)\"\n```\n\n```\nMore Help:\n\n  --select-video\n\n通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数:\n\nid=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\nsegsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\nplistDurMin=hms:plistDurMax=hms:for=FOR\n\n* for=FOR: 选择方式. best[number], worst[number], all (默认: best)\n\n例如:\n# 选择最佳视频\n-sv best\n# 选择4K+HEVC视频\n-sv res=\"3840*\":codecs=hvc1:for=best\n# 选择长度大于1小时20分钟30秒的视频\n-sv plistDurMin=\"1h20m30s\":for=best\n```\n\n```\nMore Help:\n\n  --select-audio\n\n通过正则表达式选择符合要求的音频流. 参考 --select-video\n\n例如:\n# 选择所有音频\n-sa all\n# 选择最佳英语音轨\n-sa lang=en:for=best\n# 选择最佳的2条英语(或日语)音轨\n-sa lang=\"ja|en\":for=best2\n```\n\n```\nMore Help:\n\n  --select-subtitle\n\n通过正则表达式选择符合要求的字幕流. 参考 --select-video\n\n例如:\n# 选择所有字幕\n-ss all\n# 选择所有带有\"中文\"的字幕\n-ss name=\"中文\":for=all\n```\n\n```\nMore Help:\n\n  --custom-range\n\n下载点播内容时, 仅下载部分分片.\n\n例如:\n# 下载[0,10]共11个分片\n--custom-range 0-10\n# 下载从序号10开始的后续分片\n--custom-range 10-\n# 下载前100个分片\n--custom-range -99\n# 下载第5分钟到20分钟的内容\n--custom-range 05:00-20:00\n```\n```\nMore Help:\n\n  --save-pattern\n\n使用变量设置输出文件命名模板. 支持的变量:\n\n* <SaveName>: 用户指定的保存名称 (--save-name)\n* <Id>: 流的任务ID\n* <Codecs>: 编解码器信息 (例如: avc1.64001f, mp4a.40.2)\n* <Language>: 语言代码 (例如: en, zh-CN)\n* <Resolution>: 视频分辨率 (例如: 1920x1080)\n* <Bandwidth>: 流的带宽/比特率\n* <MediaType>: 媒体类型 (VIDEO, AUDIO, SUBTITLES)\n* <Channels>: 音频声道配置\n* <FrameRate>: 帧率\n* <VideoRange>: 视频色域/HDR信息 (SDR, HDR10等)\n* <GroupId>: 流组标识符\n\n使用场景:\n当下载多个相同类型的流时(例如多个不同分辨率的视频)，使用此选项可以避免文件名冲突。\n\n例如:\n# 下载1080p和720p视频，文件名包含分辨率\n--save-pattern \"<SaveName>_<Resolution>\" --save-name \"video\"\n# 输出: video_1920x1080.mp4, video_1280x720.mp4\n\n# 包含带宽信息\n--save-pattern \"<SaveName>_<Resolution>_<Bandwidth>kbps\"\n# 输出: video_1920x1080_5000000kbps.mp4\n\n# 下载多个音频流，包含语言和声道\n--save-pattern \"<SaveName>_<Language>_<Channels>ch\"\n# 输出: audio_en_2ch.m4a, audio_es_2ch.m4a, audio_en_6ch.m4a\n\n# 复杂模板\n--save-pattern \"<MediaType>_<Resolution>_<Codecs>_<Language>\"\n# 输出: VIDEO_1920x1080_avc1.64001f_en.mp4\n\n注意:\n如果不使用 --save-pattern，程序会在文件名冲突时自动使用流的元数据(分辨率、带宽等)\n生成唯一的文件名，而不是简单地添加 \".copy\" 后缀。\n```\n\n</details>\n\n## 运行截图\n\n### 点播\n\n![RE1](img/RE.gif)\n\n还可以并行下载+自动混流\n\n![RE2](img/RE2.gif)\n\n### 直播\n\n录制TS直播源：\n\n[click to show gif](http://pan.iqiyi.com/file/paopao/W0LfmaMRvuA--uCdOpZ1cldM5JCVhMfIm7KFqr4oKCz80jLn0bBb-9PWmeCFZ-qHpAaQydQ1zk-CHYT_UbRLtw.gif)\n\n录制MPD直播源：\n\n[click to show gif](http://pan.iqiyi.com/file/paopao/nmAV5MOh0yIyHhnxdgM_6th_p2nqrFsM4k-o3cUPwUa8Eh8QOU4uyPkLa_BlBrMa3GBnKWSk8rOaUwbsjKN14g.gif)\n\n录制过程中，借助ffmpeg完成对音视频的实时混流\n\n```\nffmpeg -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\n```\n\n从 v0.1.5 开始，可以尝试开启 `live-pipe-mux` 来代替以上命令\n\n> [!NOTE]\n> 如果网络环境不够稳定，请不要开启 `live-pipe-mux`。管道内数据读取由 ffmpeg 负责，在某些环境下容易丢失直播数据。\n\n从 v0.1.8 开始，能够通过设置环境变量 `RE_LIVE_PIPE_OPTIONS` 来改变 `live-pipe-mux` 时 ffmpeg 的某些选项： <https://github.com/nilaoda/N_m3u8DL-RE/issues/162#issuecomment-1592462532>\n\n## 赞助\n\n<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>\n"
  },
  {
    "path": "TestStreams.md",
    "content": "# Test Streams\n\n* 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 (啥都有)\n* https://media.axprod.net/TestVectors/v7-Clear/Manifest_1080p.mpd (多音轨多字幕)\n* https://cmafref.akamaized.net/cmaf/live-ull/2006350/akambr/out.mpd (直播)\n* http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8 \n* https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd\n* https://dash.akamaized.net/dash264/TestCases/2c/qualcomm/1/MultiResMPEG2.mpd\n* https://livesim.dashif.org/dash/vod/testpic_2s/multi_subs.mpd (ttml + mp4)\n* http://media.axprod.net/TestVectors/v6-Clear/Manifest_1080p.mpd (vtt + mp4)\n* https://livesim.dashif.org/dash/vod/testpic_2s/xml_subs.mpd (ttml)\n* https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8 (HLS vtt)\n* https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8 (高级HLS fMP4+VTT)\n* https://events-delivery.apple.com/0205eyyhwbbqexozkwmgccegwnjyrktg/m3u8/vod_index-dpyfrsVksFWjneFiptbXnAMYBtGYbXeZ.m3u8 (高级HLS fMP4+VTT)\n* https://a38avoddashs3ww-a.akamaihd.net/ondemand/iad_2/8e91/f2f2/ec5a/430f-bd7a-0779f4a0189d/685cda75-609c-41c1-86bb-688f4cdb5521_corrected.mpd\n* https://theater.kktv.com.tw/98/04000198010001_584b26392f7f7f11fc62299214a55fb7/16113081449d8d5e9960_sub_dash.mpd (MPD+VTT)\n* https://a38avoddashs3ww-a.akamaihd.net/ondemand/iad_2/8e91/f2f2/ec5a/430f-bd7a-0779f4a0189d/685cda75-609c-41c1-86bb-688f4cdb5521_corrected.mpd\n* http://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest\n* http://amssamples.streaming.mediaservices.windows.net/683f7e47-bd83-4427-b0a3-26a6c4547782/BigBuckBunny.ism/manifest\n* https://azclwds01.akamaized.net/4e8f6858-5d05-4e28-83ab-48c7a2b259e1/XVuosg_tab_hd.ism/Manifest (`e5114f87b3e54b9faca331e6e6d646a6:55c5d9f1cedfd018b75623f2565a1d29`)\n* https://akm.eu.prd.media.max.com/bolt-glo-prod/78cccdce-2592-4a2b-a023-91034c43366e/packager-mp4-cenc/main.mpd (缺失Namespace)\n* https://cdn01.vdocipher.com/media/6YI1GS6X5lAr7/b4550743/stream.mpd (单mp4链接 无法使用Bytes: 0-)"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/DownloadSpeedColumn.cs",
    "content": "﻿using N_m3u8DL_RE.Entity;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing System.Collections.Concurrent;\nusing N_m3u8DL_RE.Common.Util;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal sealed class DownloadSpeedColumn : ProgressColumn\n{\n    private long _stopSpeed = 0;\n    private ConcurrentDictionary<int, string> DateTimeStringDic = new();\n    protected override bool NoWrap => true;\n    private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }\n\n    public DownloadSpeedColumn(ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic)\n    {\n        this.SpeedContainerDic = SpeedContainerDic;\n    }\n\n    public Style MyStyle { get; set; } = new Style(foreground: Color.Green);\n\n    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)\n    {\n        var taskId = task.Id;\n        var speedContainer = SpeedContainerDic[taskId];\n        var now = DateTime.Now.ToString(\"yyyy-MM-dd HH:mm:ss\");\n        var flag = task.IsFinished || !task.IsStarted;\n        // 单文件下载汇报进度\n        if (!flag && speedContainer is { SingleSegment: true, ResponseLength: not null })\n        {\n            task.MaxValue = (double)speedContainer.ResponseLength;\n            task.Value = speedContainer.RDownloaded;\n        }\n        // 一秒汇报一次即可\n        if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)\n        {\n            speedContainer.NowSpeed = speedContainer.Downloaded;\n            // 速度为0，计数增加\n            if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }\n            else speedContainer.ResetLowSpeedCount();\n            speedContainer.Reset();\n        }\n        DateTimeStringDic[taskId] = now;\n        var style = flag ? Style.Plain : MyStyle;\n        return flag ? new Text(\"-\", style).Centered() : new Text(GlobalUtil.FormatFileSize(speedContainer.NowSpeed) + \"ps\" + (speedContainer.LowSpeedCount > 0 ? $\"({speedContainer.LowSpeedCount})\" : \"\"), style).Centered();\n    }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/DownloadStatusColumn.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Entity;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing System.Collections.Concurrent;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal class DownloadStatusColumn : ProgressColumn\n{\n    private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }\n    private ConcurrentDictionary<int, string> DateTimeStringDic = new();\n    private ConcurrentDictionary<int, string> SizeDic = new();\n    public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);\n    public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);\n\n    public DownloadStatusColumn(ConcurrentDictionary<int, SpeedContainer> speedContainerDic)\n    {\n        this.SpeedContainerDic = speedContainerDic;\n    }\n\n    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)\n    {\n        if (task.Value == 0) return new Text(\"-\", MyStyle).RightJustified();\n        var now = DateTime.Now.ToString(\"yyyy-MM-dd HH:mm:ss\");\n\n        var speedContainer = SpeedContainerDic[task.Id];\n        var size = speedContainer.RDownloaded;\n\n        // 一秒汇报一次即可\n        if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)\n        {\n            var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));\n            SizeDic[task.Id] = $\"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}\";\n        }\n        DateTimeStringDic[task.Id] = now;\n        SizeDic.TryGetValue(task.Id, out var sizeStr);\n\n        if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);\n\n        return new Text(sizeStr ?? \"-\", MyStyle).RightJustified();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/MyPercentageColumn.cs",
    "content": "﻿using Spectre.Console.Rendering;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal class MyPercentageColumn : ProgressColumn\n{\n    /// <summary>\n    /// Gets or sets the style for a non-complete task.\n    /// </summary>\n    public Style Style { get; set; } = Style.Plain;\n\n    /// <summary>\n    /// Gets or sets the style for a completed task.\n    /// </summary>\n    public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);\n\n    /// <inheritdoc/>\n    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)\n    {\n        var percentage = task.Percentage;\n        var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;\n        return new Text($\"{task.Value}/{task.MaxValue} {percentage:F2}%\", style).RightJustified();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/RecordingDurationColumn.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Util;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing System.Collections.Concurrent;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal class RecordingDurationColumn : ProgressColumn\n{\n    protected override bool NoWrap => true;\n    private ConcurrentDictionary<int, int> _recodingDurDic;\n    private ConcurrentDictionary<int, int>? _refreshedDurDic;\n    public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey);\n    public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen);\n    public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic)\n    {\n        _recodingDurDic = recodingDurDic;\n    }\n    public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic, ConcurrentDictionary<int, int> refreshedDurDic)\n    {\n        _recodingDurDic = recodingDurDic;\n        _refreshedDurDic = refreshedDurDic;\n    }\n    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)\n    {\n        if (_refreshedDurDic == null)\n            return new Text($\"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}\", MyStyle).LeftJustified();\n        return new Text($\"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}\", GreyStyle);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/RecordingSizeColumn.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Util;\nusing Spectre.Console;\nusing Spectre.Console.Rendering;\nusing System.Collections.Concurrent;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal class RecordingSizeColumn : ProgressColumn\n{\n    protected override bool NoWrap => true;\n    private ConcurrentDictionary<int, double> RecodingSizeDic = new(); // 临时的大小 每秒刷新用\n    private ConcurrentDictionary<int, double> _recodingSizeDic;\n    private ConcurrentDictionary<int, string> DateTimeStringDic = new();\n    public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);\n    public RecordingSizeColumn(ConcurrentDictionary<int, double> recodingSizeDic)\n    {\n        _recodingSizeDic = recodingSizeDic;\n    }\n    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)\n    {\n        var now = DateTime.Now.ToString(\"yyyy-MM-dd HH:mm:ss\");\n        var taskId = task.Id;\n        // 一秒汇报一次即可\n        if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)\n        {\n            RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];\n        }\n        DateTimeStringDic[taskId] = now;\n        var flag = RecodingSizeDic.TryGetValue(taskId, out var size);\n        return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Column/RecordingStatusColumn.cs",
    "content": "﻿using Spectre.Console;\nusing Spectre.Console.Rendering;\n\nnamespace N_m3u8DL_RE.Column;\n\ninternal class RecordingStatusColumn : ProgressColumn\n{\n    protected override bool NoWrap => true;\n    public Style MyStyle { get; set; } = new Style(foreground: Color.Default);\n    public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);\n    public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)\n    {\n        if (task.IsFinished)\n            return new Text($\"{task.Value}/{task.MaxValue} Waiting  \", FinishedStyle).LeftJustified();\n        return new Text($\"{task.Value}/{task.MaxValue} Recording\", MyStyle).LeftJustified();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs",
    "content": "using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Entity;\nusing N_m3u8DL_RE.Enum;\nusing N_m3u8DL_RE.Util;\nusing System.CommandLine;\nusing System.CommandLine.Parsing;\nusing System.Globalization;\nusing System.Net;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.CommandLine;\n\ninternal static partial class CommandInvoker\n{\n    public const string VERSION_INFO = \"N_m3u8DL-RE (Beta version) 20251228\";\n\n    [GeneratedRegex(\"((best|worst)\\\\d*|all)\")]\n    private static partial Regex ForStrRegex();\n    [GeneratedRegex(@\"(\\d*)-(\\d*)\")]\n    private static partial Regex RangeRegex();\n    [GeneratedRegex(@\"([\\d\\\\.]+)(M|K)\")]\n    private static partial Regex SpeedStrRegex();\n    [GeneratedRegex(\"^[0-9a-fA-f]{32}:[0-9a-fA-f]{32}$\")]\n    private static partial Regex PairKeyRegex();\n    [GeneratedRegex(\"^[0-9]{1,}:[0-9a-fA-f]{32}$\")]\n    private static partial Regex IdHexKeyRegex();\n    [GeneratedRegex(\"^[0-9a-fA-f]{32}$\")]\n    private static partial Regex SingleHexKeyRegex();\n\n    private static readonly Argument<string> Input = new(\"input\") { Description = ResString.cmd_Input };\n    private static readonly Option<string?> TmpDir = new(\"--tmp-dir\") { Description = ResString.cmd_tmpDir };\n    private static readonly Option<string?> SaveDir = new(\"--save-dir\") { Description = ResString.cmd_saveDir };\n    private static readonly Option<string?> SaveName = new(\"--save-name\") { Description = ResString.cmd_saveName, CustomParser = ParseSaveName};\n    private static readonly Option<string?> SavePattern = new(\"--save-pattern\") { Description = ResString.cmd_savePattern };\n    private static readonly Option<string?> LogFilePath = new(\"--log-file-path\") { Description = ResString.cmd_logFilePath, CustomParser = ParseFilePath};\n    private static readonly Option<string?> UILanguage = new Option<string?>(\"--ui-language\") { Description = ResString.cmd_uiLanguage }.AcceptOnlyFromAmong(\"en-US\", \"zh-CN\", \"zh-TW\");\n    private static readonly Option<string?> UrlProcessorArgs = new(\"--urlprocessor-args\") { Description = ResString.cmd_urlProcessorArgs };\n    private static readonly Option<string> KeyTextFile = new(\"--key-text-file\") { Description = ResString.cmd_keyText };\n    private static readonly Option<Dictionary<string, string>> Headers = new(\"-H\", \"--header\") { HelpName = \"header\", Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, Description = ResString.cmd_header, CustomParser = ParseHeaders };\n    private static readonly Option<LogLevel> LogLevel = new(\"--log-level\") { Description = ResString.cmd_logLevel, DefaultValueFactory = _ => Common.Log.LogLevel.INFO };\n    private static readonly Option<SubtitleFormat> SubtitleFormat = new(\"--sub-format\") { Description = ResString.cmd_subFormat, DefaultValueFactory = _ => Enum.SubtitleFormat.SRT };\n    private static readonly Option<bool> DisableUpdateCheck = new Option<bool>(\"--disable-update-check\") { Description = ResString.cmd_disableUpdateCheck }.WithDefault(false);\n    private static readonly Option<bool> AutoSelect = new Option<bool>(\"--auto-select\") { Description = ResString.cmd_autoSelect }.WithDefault(false);\n    private static readonly Option<bool> SubOnly = new Option<bool>(\"--sub-only\") { Description = ResString.cmd_subOnly }.WithDefault(false);\n    private static readonly Option<int> ThreadCount = new(\"--thread-count\") { HelpName = \"number\", Description = ResString.cmd_threadCount, DefaultValueFactory = _ => Environment.ProcessorCount };\n    private static readonly Option<int> DownloadRetryCount = new(\"--download-retry-count\") { HelpName = \"number\", Description = ResString.cmd_downloadRetryCount, DefaultValueFactory = _ => 3 };\n    private static readonly Option<double> HttpRequestTimeout = new(\"--http-request-timeout\") { HelpName = \"seconds\", Description = ResString.cmd_httpRequestTimeout, DefaultValueFactory = _ => 100 };\n    private static readonly Option<bool> SkipMerge = new Option<bool>(\"--skip-merge\") { Description = ResString.cmd_skipMerge }.WithDefault(false);\n    private static readonly Option<bool> SkipDownload = new Option<bool>(\"--skip-download\") { Description = ResString.cmd_skipDownload }.WithDefault(false);\n    private static readonly Option<bool> NoDateInfo = new Option<bool>(\"--no-date-info\") { Description = ResString.cmd_noDateInfo }.WithDefault(false);\n    private static readonly Option<bool> BinaryMerge = new Option<bool>(\"--binary-merge\") { Description = ResString.cmd_binaryMerge }.WithDefault(false);\n    private static readonly Option<bool> UseFFmpegConcatDemuxer = new Option<bool>(\"--use-ffmpeg-concat-demuxer\") { Description = ResString.cmd_useFFmpegConcatDemuxer }.WithDefault(false);\n    private static readonly Option<bool> DelAfterDone = new Option<bool>(\"--del-after-done\") { Description = ResString.cmd_delAfterDone }.WithDefault(true);\n    private static readonly Option<bool> AutoSubtitleFix = new Option<bool>(\"--auto-subtitle-fix\") { Description = ResString.cmd_subtitleFix }.WithDefault(true);\n    private static readonly Option<bool> CheckSegmentsCount = new Option<bool>(\"--check-segments-count\") { Description = ResString.cmd_checkSegmentsCount }.WithDefault(true);\n    private static readonly Option<bool> WriteMetaJson = new Option<bool>(\"--write-meta-json\") { Description = ResString.cmd_writeMetaJson }.WithDefault(true);\n    private static readonly Option<bool> AppendUrlParams = new Option<bool>(\"--append-url-params\") { Description = ResString.cmd_appendUrlParams }.WithDefault(false);\n    private static readonly Option<bool> MP4RealTimeDecryption = new Option<bool>(\"--mp4-real-time-decryption\") { Description = ResString.cmd_MP4RealTimeDecryption }.WithDefault(false);\n    private static readonly Option<bool> UseShakaPackager = new Option<bool>(\"--use-shaka-packager\") { Hidden = true, Description = ResString.cmd_useShakaPackager }.WithDefault(false);\n    private static readonly Option<DecryptEngine> DecryptionEngine = new (\"--decryption-engine\") { Description = ResString.cmd_decryptionEngine, DefaultValueFactory = _ => DecryptEngine.MP4DECRYPT };\n    private static readonly Option<bool> ForceAnsiConsole = new(\"--force-ansi-console\") { Description = ResString.cmd_forceAnsiConsole };\n    private static readonly Option<bool> NoAnsiColor = new(\"--no-ansi-color\") { Description = ResString.cmd_noAnsiColor };\n    private static readonly Option<string?> DecryptionBinaryPath = new(\"--decryption-binary-path\") { HelpName = \"PATH\", Description = ResString.cmd_decryptionBinaryPath };\n    private static readonly Option<string?> FFmpegBinaryPath = new(\"--ffmpeg-binary-path\") { HelpName = \"PATH\", Description = ResString.cmd_ffmpegBinaryPath };\n    private static readonly Option<string?> BaseUrl = new(\"--base-url\") { Description = ResString.cmd_baseUrl };\n    private static readonly Option<bool> ConcurrentDownload = new Option<bool>(\"-mt\", \"--concurrent-download\") { Description = ResString.cmd_concurrentDownload }.WithDefault(false);\n    private static readonly Option<bool> NoLog = new Option<bool>(\"--no-log\") { Description = ResString.cmd_noLog }.WithDefault(false);\n    private static readonly Option<bool> AllowHlsMultiExtMap = new Option<bool>(\"--allow-hls-multi-ext-map\") { Description = ResString.cmd_allowHlsMultiExtMap }.WithDefault(false);\n    private static readonly Option<string[]?> AdKeywords = new(\"--ad-keyword\") { HelpName = \"REG\", Description = ResString.cmd_adKeyword };\n    private static readonly Option<long?> MaxSpeed = new(\"-R\", \"--max-speed\") { HelpName = \"SPEED\", Description = ResString.cmd_maxSpeed, CustomParser = ParseSpeedLimit };\n\n\n    // 代理选项\n    private static readonly Option<bool> UseSystemProxy = new Option<bool>(\"--use-system-proxy\") { Description = ResString.cmd_useSystemProxy }.WithDefault(true);\n    private static readonly Option<WebProxy?> CustomProxy = new(\"--custom-proxy\") { HelpName = \"URL\", Description = ResString.cmd_customProxy, CustomParser = ParseProxy};\n\n    // 只下载部分分片\n    private static readonly Option<CustomRange?> CustomRange = new(\"--custom-range\") { HelpName = \"RANGE\", Description = ResString.cmd_customRange, CustomParser = ParseCustomRange };\n\n\n    // morehelp\n    private static readonly Option<string?> MoreHelp = new(\"--morehelp\") { HelpName = \"OPTION\", Description = ResString.cmd_moreHelp };\n\n    // 自定义KEY等\n    private static readonly Option<EncryptMethod?> CustomHLSMethod = new(\"--custom-hls-method\") { HelpName = \"METHOD\", Description = ResString.cmd_customHLSMethod };\n    private static readonly Option<byte[]?> CustomHLSKey = new(\"--custom-hls-key\") { HelpName = \"FILE|HEX|BASE64\", Description = ResString.cmd_customHLSKey, CustomParser = ParseHLSCustomKey };\n    private static readonly Option<byte[]?> CustomHLSIv = new(name: \"--custom-hls-iv\") { HelpName = \"FILE|HEX|BASE64\", Description = ResString.cmd_customHLSIv, CustomParser = ParseHLSCustomKey };\n    private static readonly Option<string[]?> Keys = new(\"--key\") { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, Description = ResString.cmd_keys, CustomParser = ParseCustomKeys};\n\n    // 任务开始时间\n    private static readonly Option<DateTime?> TaskStartAt = new(\"--task-start-at\") { HelpName = \"yyyyMMddHHmmss\", Description = ResString.cmd_taskStartAt, CustomParser = ParseStartTime };\n\n\n    // 直播相关\n    private static readonly Option<bool> LivePerformAsVod = new Option<bool>(\"--live-perform-as-vod\") { Description = ResString.cmd_livePerformAsVod }.WithDefault(false);\n    private static readonly Option<bool> LiveRealTimeMerge = new Option<bool>(\"--live-real-time-merge\") { Description = ResString.cmd_liveRealTimeMerge }.WithDefault(false);\n    private static readonly Option<bool> LiveKeepSegments = new Option<bool>(\"--live-keep-segments\") { Description = ResString.cmd_liveKeepSegments }.WithDefault(true);\n    private static readonly Option<bool> LivePipeMux = new Option<bool>(\"--live-pipe-mux\") { Description = ResString.cmd_livePipeMux }.WithDefault(false);\n    private static readonly Option<TimeSpan?> LiveRecordLimit = new(\"--live-record-limit\") { HelpName = \"HH:mm:ss\", Description = ResString.cmd_liveRecordLimit, CustomParser = ParseLiveLimit };\n    private static readonly Option<int?> LiveWaitTime = new(\"--live-wait-time\") { HelpName = \"SEC\", Description = ResString.cmd_liveWaitTime };\n    private static readonly Option<int> LiveTakeCount = new(\"--live-take-count\") { HelpName = \"NUM\", Description = ResString.cmd_liveTakeCount, DefaultValueFactory = _ => 16 };\n    private static readonly Option<bool> LiveFixVttByAudio = new Option<bool>(\"--live-fix-vtt-by-audio\") { Description = ResString.cmd_liveFixVttByAudio }.WithDefault(false);\n\n\n    // 复杂命令行如下\n    private static readonly Option<MuxOptions?> MuxAfterDone = new(\"-M\", \"--mux-after-done\") { HelpName = \"OPTIONS\", Description = ResString.cmd_muxAfterDone, CustomParser = ParseMuxAfterDone };\n    private static readonly Option<List<OutputFile>> MuxImports = new(\"--mux-import\") { HelpName = \"OPTIONS\", Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, Description = ResString.cmd_muxImport, CustomParser = ParseImports };\n    private static readonly Option<StreamFilter?> VideoFilter = new(\"-sv\", \"--select-video\") { HelpName = \"OPTIONS\", Description = ResString.cmd_selectVideo, CustomParser = ParseStreamFilter };\n    private static readonly Option<StreamFilter?> AudioFilter = new(\"-sa\", \"--select-audio\") { HelpName = \"OPTIONS\", Description = ResString.cmd_selectAudio, CustomParser = ParseStreamFilter };\n    private static readonly Option<StreamFilter?> SubtitleFilter = new(\"-ss\", \"--select-subtitle\") { HelpName = \"OPTIONS\", Description = ResString.cmd_selectSubtitle, CustomParser = ParseStreamFilter };\n\n    private static readonly Option<StreamFilter?> DropVideoFilter = new(\"-dv\", \"--drop-video\") { HelpName = \"OPTIONS\", Description = ResString.cmd_dropVideo, CustomParser = ParseStreamFilter };\n    private static readonly Option<StreamFilter?> DropAudioFilter = new(\"-da\", \"--drop-audio\") { HelpName = \"OPTIONS\", Description = ResString.cmd_dropAudio, CustomParser = ParseStreamFilter };\n    private static readonly Option<StreamFilter?> DropSubtitleFilter = new(\"-ds\", \"--drop-subtitle\") { HelpName = \"OPTIONS\", Description = ResString.cmd_dropSubtitle, CustomParser = ParseStreamFilter };\n\n    /// <summary>\n    /// 解析下载速度限制\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static long? ParseSpeedLimit(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value.ToUpper();\n        try\n        {\n            var reg = SpeedStrRegex();\n            if (!reg.IsMatch(input)) throw new ArgumentException($\"Invalid Speed Limit: {input}\");\n\n            var number = double.Parse(reg.Match(input).Groups[1].Value);\n            if (reg.Match(input).Groups[2].Value == \"M\")\n                return (long)(number * 1024 * 1024);\n            return (long)(number * 1024);\n        }\n        catch (Exception)\n        {\n            result.AddError(\"error in parse SpeedLimit: \" + input);\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// 解析用户定义的下载范围\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"ArgumentException\"></exception>\n    private static CustomRange? ParseCustomRange(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value;\n        // 支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00;\n        try\n        {\n            if (string.IsNullOrEmpty(input))\n                return null;\n\n            var arr = input.Split('-');\n            if (arr.Length != 2)\n                throw new ArgumentException(\"Bad format!\");\n\n            if (input.Contains(':'))\n            {\n                return new CustomRange()\n                {\n                    InputStr = input,\n                    StartSec = arr[0] == \"\" ? 0 : OtherUtil.ParseDur(arr[0]).TotalSeconds,\n                    EndSec = arr[1] == \"\" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds,\n                };\n            }\n\n            if (RangeRegex().IsMatch(input))\n            {\n                var left = RangeRegex().Match(input).Groups[1].Value;\n                var right = RangeRegex().Match(input).Groups[2].Value;\n                return new CustomRange()\n                {\n                    InputStr = input,\n                    StartSegIndex = left == \"\" ? 0 : long.Parse(left),\n                    EndSegIndex = right == \"\" ? long.MaxValue : long.Parse(right),\n                };\n            }\n\n            throw new ArgumentException(\"Bad format!\");\n        }\n        catch (Exception ex)\n        {\n            result.AddError(\"error in parse CustomRange: \" + ex.Message);\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// 解析用户代理\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"ArgumentException\"></exception>\n    private static WebProxy? ParseProxy(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value;\n        try\n        {\n            if (string.IsNullOrEmpty(input))\n                return null;\n\n            var uri = new Uri(input);\n            var proxy = new WebProxy(uri, true);\n            if (!string.IsNullOrEmpty(uri.UserInfo))\n            {\n                var infos = uri.UserInfo.Split(':');\n                proxy.Credentials = new NetworkCredential(infos.First(), infos.Last());\n            }\n            return proxy;\n        }\n        catch (Exception ex)\n        {\n            result.AddError(\"error in parse proxy: \" + ex.Message);\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// 解析自定义KEY（用于mp4decrypt等第三方程序）\n    /// 支持格式：<br/>\n    /// - KEY（hex）<br/>\n    /// - KID:KEY（hex）<br/>\n    /// - Base64KEY<br/>\n    /// - Base64KID:Base64KEY\n    /// </summary>\n    private static string[]? ParseCustomKeys(ArgumentResult result)\n    {\n        const int KeyBytes = 16;\n        const int KeyHexLen = KeyBytes * 2;\n        \n        string ParsePart(string part, string label)\n        {\n            if (SingleHexKeyRegex().IsMatch(part))\n                return part.ToLowerInvariant();\n\n            if (HexUtil.TryParseBase64(part, out var hex) && hex is { Length: KeyHexLen })\n                return hex.ToLowerInvariant();\n\n            throw new ArgumentException($\"{label} must be valid 16-byte HEX or Base64. Input string: {part}\");\n        }\n\n        var keys = new List<string>();\n        var inputs = result.Tokens.Select(t => t.Value).ToList();\n\n        try\n        {\n            foreach (var input in inputs)\n            {\n                // 已匹配标准格式的，直接添加\n                if (PairKeyRegex().IsMatch(input) || IdHexKeyRegex().IsMatch(input) || SingleHexKeyRegex().IsMatch(input))\n                {\n                    keys.Add(input);\n                    continue;\n                }\n\n                // 拆分KID:KEY\n                var parts = input.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n\n                if (parts.Length is < 1 or > 2)\n                    throw new ArgumentException(\"Input must be KEY or KID:KEY format.\");\n\n                if (parts.Length == 1)\n                {\n                    var key = ParsePart(parts[0], \"KEY\");\n                    keys.Add(key);\n                }\n                else // KID:KEY\n                {\n                    var kid = ParsePart(parts[0], \"KID\");\n                    var key = ParsePart(parts[1], \"KEY\");\n                    keys.Add($\"{kid}:{key}\");\n                }\n            }\n\n            return [.. keys];\n        }\n        catch (Exception ex)\n        {\n            result.AddError($\"error in parse custom key: {ex.Message}. All Inputs=[{string.Join(\", \", inputs)}]\");\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// 解析自定义KEY\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static byte[]? ParseHLSCustomKey(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value;\n        try\n        {\n            if (string.IsNullOrEmpty(input))\n                return null;\n            if (File.Exists(input))\n                return File.ReadAllBytes(input);\n            if (HexUtil.TryParseHexString(input, out byte[]? bytes))\n                return bytes;\n            return Convert.FromBase64String(input);\n        }\n        catch (Exception)\n        {\n            result.AddError(\"error in parse hls custom key: \" + input);\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// 解析录制直播时长限制\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static TimeSpan? ParseLiveLimit(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value;\n        try\n        {\n            return OtherUtil.ParseDur(input);\n        }\n        catch (Exception)\n        {\n            result.AddError(\"error in parse LiveRecordLimit: \" + input);\n            return null;\n        }\n    }\n\n    /// <summary>\n    /// 解析任务开始时间\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static DateTime? ParseStartTime(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value;\n        try\n        {\n            CultureInfo provider = CultureInfo.InvariantCulture;\n            return DateTime.ParseExact(input, \"yyyyMMddHHmmss\", provider);\n        }\n        catch (Exception)\n        {\n            result.AddError(\"error in parse TaskStartTime: \" + input);\n            return null;\n        }\n    }\n\n    private static string? ParseSaveName(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value;\n        var newName = OtherUtil.GetValidFileName(input);\n        if (string.IsNullOrEmpty(newName))\n        {\n            result.AddError(\"Invalid save name!\");\n            return null;\n        }\n        return newName;\n    }\n\n    private static string? ParseFilePath(ArgumentResult result)\n    {\n        var input = result.Tokens[0].Value;\n        var path = \"\";\n        try\n        {\n            path = Path.GetFullPath(input);\n        }\n        catch (Exception e)\n        {\n            result.AddError(\"Invalid log path!\");\n            return null;\n        }\n        var dir = Path.GetDirectoryName(path);\n        var filename = Path.GetFileName(path);\n        var newName = OtherUtil.GetValidFileName(filename);\n        if (string.IsNullOrEmpty(newName))\n        {\n            result.AddError(\"Invalid log file name!\");\n            return null;\n        }\n        return Path.Combine(dir!, newName);\n    }\n\n    /// <summary>\n    /// 流过滤器\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static StreamFilter? ParseStreamFilter(ArgumentResult result)\n    {\n        var streamFilter = new StreamFilter();\n        var input = result.Tokens[0].Value;\n        var p = new ComplexParamParser(input);\n\n\n        // 目标范围\n        var forStr = \"\";\n        if (input == ForStrRegex().Match(input).Value)\n        {\n            forStr = input;\n        }\n        else\n        {\n            forStr = p.GetValue(\"for\") ?? \"best\";\n            if (forStr != ForStrRegex().Match(forStr).Value)\n            {\n                result.AddError($\"for={forStr} not valid\");\n                return null;\n            }\n        }\n        streamFilter.For = forStr;\n\n        var id = p.GetValue(\"id\");\n        if (!string.IsNullOrEmpty(id))\n            streamFilter.GroupIdReg = new Regex(id);\n\n        var lang = p.GetValue(\"lang\");\n        if (!string.IsNullOrEmpty(lang))\n            streamFilter.LanguageReg = new Regex(lang);\n\n        var name = p.GetValue(\"name\");\n        if (!string.IsNullOrEmpty(name))\n            streamFilter.NameReg = new Regex(name);\n\n        var codecs = p.GetValue(\"codecs\");\n        if (!string.IsNullOrEmpty(codecs))\n            streamFilter.CodecsReg = new Regex(codecs);\n\n        var res = p.GetValue(\"res\");\n        if (!string.IsNullOrEmpty(res))\n            streamFilter.ResolutionReg = new Regex(res);\n\n        var frame = p.GetValue(\"frame\");\n        if (!string.IsNullOrEmpty(frame))\n            streamFilter.FrameRateReg = new Regex(frame);\n\n        var channel = p.GetValue(\"channel\");\n        if (!string.IsNullOrEmpty(channel))\n            streamFilter.ChannelsReg = new Regex(channel);\n\n        var range = p.GetValue(\"range\");\n        if (!string.IsNullOrEmpty(range))\n            streamFilter.VideoRangeReg = new Regex(range);\n\n        var url = p.GetValue(\"url\");\n        if (!string.IsNullOrEmpty(url))\n            streamFilter.UrlReg = new Regex(url);\n\n        var segsMin = p.GetValue(\"segsMin\");\n        if (!string.IsNullOrEmpty(segsMin))\n            streamFilter.SegmentsMinCount = long.Parse(segsMin);\n\n        var segsMax = p.GetValue(\"segsMax\");\n        if (!string.IsNullOrEmpty(segsMax))\n            streamFilter.SegmentsMaxCount = long.Parse(segsMax);\n\n        var plistDurMin = p.GetValue(\"plistDurMin\");\n        if (!string.IsNullOrEmpty(plistDurMin))\n            streamFilter.PlaylistMinDur = OtherUtil.ParseSeconds(plistDurMin);\n\n        var plistDurMax = p.GetValue(\"plistDurMax\");\n        if (!string.IsNullOrEmpty(plistDurMax))\n            streamFilter.PlaylistMaxDur = OtherUtil.ParseSeconds(plistDurMax);\n\n        var bwMin = p.GetValue(\"bwMin\");\n        if (!string.IsNullOrEmpty(bwMin))\n            streamFilter.BandwidthMin = int.Parse(bwMin) * 1000;\n\n        var bwMax = p.GetValue(\"bwMax\");\n        if (!string.IsNullOrEmpty(bwMax))\n            streamFilter.BandwidthMax = int.Parse(bwMax) * 1000;\n\n        var role = p.GetValue(\"role\");\n        if (System.Enum.TryParse(role, true, out RoleType roleType))\n            streamFilter.Role = roleType;\n\n        return streamFilter;\n    }\n\n    /// <summary>\n    /// 分割Header\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static Dictionary<string, string> ParseHeaders(ArgumentResult result)\n    {\n        var array = result.Tokens.Select(t => t.Value).ToArray();\n        return OtherUtil.SplitHeaderArrayToDic(array);\n    }\n\n    /// <summary>\n    /// 解析混流引入的外部文件\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static List<OutputFile> ParseImports(ArgumentResult result)\n    {\n        var imports = new List<OutputFile>();\n\n        foreach (var item in result.Tokens)\n        {\n            var p = new ComplexParamParser(item.Value);\n            var path = p.GetValue(\"path\") ?? item.Value; // 若未获取到，直接整个字符串作为path\n            var lang = p.GetValue(\"lang\");\n            var name = p.GetValue(\"name\");\n            if (string.IsNullOrEmpty(path) || !File.Exists(path))\n            {\n                result.AddError(\"path empty or file not exists!\");\n                return imports;\n            }\n            imports.Add(new OutputFile()\n            {\n                Index = 999,\n                FilePath = path,\n                LangCode = lang,\n                Description = name\n            });\n        }\n\n        return imports;\n    }\n\n    /// <summary>\n    /// 解析混流选项\n    /// </summary>\n    /// <param name=\"result\"></param>\n    /// <returns></returns>\n    private static MuxOptions? ParseMuxAfterDone(ArgumentResult result)\n    {\n        var v = result.Tokens[0].Value;\n        var p = new ComplexParamParser(v);\n        // 混流格式\n        var format = p.GetValue(\"format\") ?? v.Split(':')[0]; // 若未获取到，直接:前的字符串作为format解析\n        var parseResult = System.Enum.TryParse(format.ToUpperInvariant(), out MuxFormat muxFormat);\n        if (!parseResult)\n        {\n            result.AddError($\"format={format} not valid\");\n            return null;\n        }\n        // 混流器\n        var muxer = p.GetValue(\"muxer\") ?? \"ffmpeg\";\n        if (muxer != \"ffmpeg\" && muxer != \"mkvmerge\")\n        {\n            result.AddError($\"muxer={muxer} not valid\");\n            return null;\n        }\n        // 混流器路径\n        var bin_path = p.GetValue(\"bin_path\") ?? \"auto\";\n        if (string.IsNullOrEmpty(bin_path))\n        {\n            result.AddError($\"bin_path={bin_path} not valid\");\n            return null;\n        }\n        // 是否删除\n        var keep = p.GetValue(\"keep\") ?? \"false\";\n        if (keep != \"true\" && keep != \"false\")\n        {\n            result.AddError($\"keep={keep} not valid\");\n            return null;\n        }\n        // 是否忽略字幕\n        var skipSub = p.GetValue(\"skip_sub\") ?? \"false\";\n        if (skipSub != \"true\" && skipSub != \"false\")\n        {\n            result.AddError($\"skip_sub={keep} not valid\");\n            return null;\n        }\n        // 冲突检测\n        if (muxer == \"mkvmerge\" && format == \"mp4\")\n        {\n            result.AddError($\"mkvmerge can not do mp4\");\n            return null;\n        }\n        return new MuxOptions()\n        {\n            UseMkvmerge = muxer == \"mkvmerge\",\n            MuxFormat = muxFormat,\n            KeepFiles = keep == \"true\",\n            SkipSubtitle = skipSub == \"true\",\n            BinPath = bin_path == \"auto\" ? null : bin_path\n        };\n    }\n\n    private static bool HasOption(this ParseResult result, Option option)\n    {\n        var allTokens = result.Tokens.Select(x => x.Value).ToList();\n        List<string> optionNames = [option.Name, ..option.Aliases];\n        return optionNames.Any(x => allTokens.Contains(x));\n    }\n    \n    private static Option<T> WithDefault<T>(this Option<T> option, T defaultValue)\n    {\n        if (option is not Option<bool>)\n            return option;\n        option.DefaultValueFactory = _ => defaultValue;\n        var currentDesc = option.Description ?? string.Empty;\n        var defaultText = defaultValue?.ToString() ?? \"null\";\n        // 拼接：原描述 + 空格 + [default: ...]\n        option.Description = string.IsNullOrWhiteSpace(currentDesc)\n            ? $\"[default: {defaultText}]\"\n            : $\"{currentDesc.Trim()} [default: {defaultText}]\";\n        return option;\n    }\n\n    private static MyOption GetOptions(ParseResult result)\n    {\n        var option = new MyOption\n        {\n            Input = result.GetRequiredValue(Input),\n            ForceAnsiConsole = result.GetValue(ForceAnsiConsole),\n            NoAnsiColor = result.GetValue(NoAnsiColor),\n            LogLevel = result.GetValue(LogLevel),\n            AutoSelect = result.GetValue(AutoSelect),\n            DisableUpdateCheck = result.GetValue(DisableUpdateCheck),\n            SkipMerge = result.GetValue(SkipMerge),\n            BinaryMerge = result.GetValue(BinaryMerge),\n            UseFFmpegConcatDemuxer = result.GetValue(UseFFmpegConcatDemuxer),\n            DelAfterDone = result.GetValue(DelAfterDone),\n            AutoSubtitleFix = result.GetValue(AutoSubtitleFix),\n            CheckSegmentsCount = result.GetValue(CheckSegmentsCount),\n            SubtitleFormat = result.GetValue(SubtitleFormat),\n            SubOnly = result.GetValue(SubOnly),\n            TmpDir = result.GetValue(TmpDir),\n            SaveDir = result.GetValue(SaveDir),\n            SaveName = result.GetValue(SaveName),\n            LogFilePath = result.GetValue(LogFilePath),\n            ThreadCount = result.GetValue(ThreadCount),\n            UILanguage = result.GetValue(UILanguage),\n            SkipDownload = result.GetValue(SkipDownload),\n            WriteMetaJson = result.GetValue(WriteMetaJson),\n            AppendUrlParams = result.GetValue(AppendUrlParams),\n            SavePattern = result.GetValue(SavePattern),\n            Keys = result.GetValue(Keys),\n            UrlProcessorArgs = result.GetValue(UrlProcessorArgs),\n            MP4RealTimeDecryption = result.GetValue(MP4RealTimeDecryption),\n            UseShakaPackager = result.GetValue(UseShakaPackager),\n            DecryptionEngine = result.GetValue(DecryptionEngine),\n            DecryptionBinaryPath = result.GetValue(DecryptionBinaryPath),\n            FFmpegBinaryPath = result.GetValue(FFmpegBinaryPath),\n            KeyTextFile = result.GetValue(KeyTextFile),\n            DownloadRetryCount = result.GetValue(DownloadRetryCount),\n            HttpRequestTimeout = result.GetValue(HttpRequestTimeout),\n            BaseUrl = result.GetValue(BaseUrl),\n            MuxImports = result.GetValue(MuxImports),\n            ConcurrentDownload = result.GetValue(ConcurrentDownload),\n            VideoFilter = result.GetValue(VideoFilter),\n            AudioFilter = result.GetValue(AudioFilter),\n            SubtitleFilter = result.GetValue(SubtitleFilter),\n            DropVideoFilter = result.GetValue(DropVideoFilter),\n            DropAudioFilter = result.GetValue(DropAudioFilter),\n            DropSubtitleFilter = result.GetValue(DropSubtitleFilter),\n            LiveRealTimeMerge = result.GetValue(LiveRealTimeMerge),\n            LiveKeepSegments = result.GetValue(LiveKeepSegments),\n            LiveRecordLimit = result.GetValue(LiveRecordLimit),\n            TaskStartAt = result.GetValue(TaskStartAt),\n            LivePerformAsVod = result.GetValue(LivePerformAsVod),\n            LivePipeMux = result.GetValue(LivePipeMux),\n            LiveFixVttByAudio = result.GetValue(LiveFixVttByAudio),\n            UseSystemProxy = result.GetValue(UseSystemProxy),\n            CustomProxy = result.GetValue(CustomProxy),\n            CustomRange = result.GetValue(CustomRange),\n            LiveWaitTime = result.GetValue(LiveWaitTime),\n            LiveTakeCount = result.GetValue(LiveTakeCount),\n            NoDateInfo = result.GetValue(NoDateInfo),\n            NoLog = result.GetValue(NoLog),\n            AllowHlsMultiExtMap = result.GetValue(AllowHlsMultiExtMap),\n            AdKeywords = result.GetValue(AdKeywords),\n            MaxSpeed = result.GetValue(MaxSpeed),\n        };\n\n        if (result.HasOption(CustomHLSMethod)) option.CustomHLSMethod = result.GetValue(CustomHLSMethod);\n        if (result.HasOption(CustomHLSKey)) option.CustomHLSKey = result.GetValue(CustomHLSKey);\n        if (result.HasOption(CustomHLSIv)) option.CustomHLSIv = result.GetValue(CustomHLSIv);\n\n        var parsedHeaders = result.GetValue(Headers);\n        if (parsedHeaders != null)\n            option.Headers = parsedHeaders;\n\n\n        // 以用户选择语言为准优先\n        if (option.UILanguage != null)\n        {\n            CultureUtil.ChangeCurrentCultureName(option.UILanguage);\n        }\n\n        // 混流设置\n        var muxAfterDoneValue = result.GetValue(MuxAfterDone);\n        if (muxAfterDoneValue == null) return option;\n        \n        option.MuxAfterDone = true;\n        option.MuxOptions = muxAfterDoneValue;\n        if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath;\n        else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath;\n\n        return option;\n    }\n\n\n    public static async Task<int> InvokeArgs(string[] args, Func<MyOption, Task> action)\n    {\n        var argList = new List<string>(args);\n        var index = -1;\n        if ((index = argList.IndexOf(\"--morehelp\")) >= 0 && argList.Count > index + 1)\n        {\n            var option = argList[index + 1];\n            var msg = option switch\n            {\n                \"mux-after-done\" => ResString.cmd_muxAfterDone_more,\n                \"mux-import\" => ResString.cmd_muxImport_more,\n                \"select-video\" => ResString.cmd_selectVideo_more,\n                \"select-audio\" => ResString.cmd_selectAudio_more,\n                \"select-subtitle\" => ResString.cmd_selectSubtitle_more,\n                \"custom-range\" => ResString.cmd_custom_range,\n                _ => $\"Option=\\\"{option}\\\" not found\"\n            };\n            Console.WriteLine($\"More Help:\\r\\n\\r\\n  --{option}\\r\\n\\r\\n\" + msg);\n            Environment.Exit(0);\n        }\n\n        var rootCommand = new RootCommand(VERSION_INFO)\n        {\n            Input, TmpDir, SaveDir, SaveName, SavePattern, LogFilePath, BaseUrl, ThreadCount, DownloadRetryCount, HttpRequestTimeout, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,\n            BinaryMerge, UseFFmpegConcatDemuxer, DelAfterDone, NoDateInfo, NoLog, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, SubOnly, SubtitleFormat, AutoSubtitleFix,\n            FFmpegBinaryPath,\n            LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionEngine, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption,\n            MaxSpeed,\n            MuxAfterDone,\n            CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, CustomRange, TaskStartAt,\n            LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveFixVttByAudio, LiveRecordLimit, LiveWaitTime, LiveTakeCount,\n            MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, AdKeywords, DisableUpdateCheck, AllowHlsMultiExtMap, MoreHelp\n        };\n\n        rootCommand.TreatUnmatchedTokensAsErrors = true;\n        rootCommand.SetAction(parseResult =>\n        {\n            var myOption = GetOptions(parseResult);\n            return action(myOption);\n        });\n\n        var config = new ParserConfiguration\n        {\n            EnablePosixBundling = false\n        };\n\n        try\n        {\n            var parseResult = rootCommand.Parse(args, config);\n            var exitCode = await parseResult.InvokeAsync();\n            Environment.Exit(exitCode);\n        }\n        catch (Exception ex)\n        {\n            var msg = Logger.LogLevel == Common.Log.LogLevel.DEBUG \n                ? ex.ToString() \n                : ex.Message;\n#if DEBUG\n            msg = ex.ToString();\n#endif\n            Logger.Error(msg);\n            Thread.Sleep(3000);\n            Environment.Exit(1);\n        }\n        finally\n        {\n            try { Console.CursorVisible = true; } catch { }\n        }\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE/CommandLine/ComplexParamParser.cs",
    "content": "﻿using System.Text;\n\nnamespace N_m3u8DL_RE.CommandLine;\n\ninternal class ComplexParamParser\n{\n    private readonly string _arg;\n    public ComplexParamParser(string arg)\n    {\n        _arg = arg;\n    }\n\n    public string? GetValue(string key)\n    {\n        if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(_arg)) return null;\n\n        try\n        {\n            var index = _arg.IndexOf(key + \"=\", StringComparison.Ordinal);\n            if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? \"true\" : null;\n\n            var chars = _arg[(index + key.Length + 1)..].ToCharArray();\n            var result = new StringBuilder();\n            char last = '\\0';\n            for (int i = 0; i < chars.Length; i++)\n            {\n                if (chars[i] == ':')\n                {\n                    if (last == '\\\\')\n                    {\n                        result.Replace(\"\\\\\", \"\");\n                        last = chars[i];\n                        result.Append(chars[i]);\n                    }\n                    else break;\n                }\n                else\n                {\n                    last = chars[i];\n                    result.Append(chars[i]);\n                }\n            }\n\n            var resultStr = result.ToString().Trim().Trim('\\\"').Trim('\\'');\n\n            // 不应该有引号出现\n            if (resultStr.Contains('\\\"') || resultStr.Contains('\\'')) throw new Exception();\n\n            return resultStr;\n        }\n        catch (Exception)\n        {\n            throw new ArgumentException($\"Parse Argument [{key}] failed!\");\n        }\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/CommandLine/MyOption.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Entity;\nusing N_m3u8DL_RE.Enum;\nusing System.Net;\n\nnamespace N_m3u8DL_RE.CommandLine;\n\ninternal class MyOption\n{\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.Input\"/>.\n    /// </summary>\n    public string Input { get; set; } = default!;\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.Headers\"/>.\n    /// </summary>\n    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.AdKeywords\"/>.\n    /// </summary>\n    public string[]? AdKeywords { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.MaxSpeed\"/>.\n    /// </summary>\n    public long? MaxSpeed { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.Keys\"/>.\n    /// </summary>\n    public string[]? Keys { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.BaseUrl\"/>.\n    /// </summary>\n    public string? BaseUrl { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.KeyTextFile\"/>.\n    /// </summary>\n    public string? KeyTextFile { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.UrlProcessorArgs\"/>.\n    /// </summary>\n    public string? UrlProcessorArgs { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LogLevel\"/>.\n    /// </summary>\n    public LogLevel LogLevel { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.NoDateInfo\"/>.\n    /// </summary>\n    public bool NoDateInfo { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.NoLog\"/>.\n    /// </summary>\n    public bool NoLog { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.AllowHlsMultiExtMap\"/>.\n    /// </summary>\n    public bool AllowHlsMultiExtMap { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.AutoSelect\"/>.\n    /// </summary>\n    public bool AutoSelect { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DisableUpdateCheck\"/>.\n    /// </summary>\n    public bool DisableUpdateCheck { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SubOnly\"/>.\n    /// </summary>\n    public bool SubOnly { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.ThreadCount\"/>.\n    /// </summary>\n    public int ThreadCount { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DownloadRetryCount\"/>.\n    /// </summary>\n    public int DownloadRetryCount { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.HttpRequestTimeout\"/>.\n    /// </summary>\n    public double HttpRequestTimeout { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LiveRecordLimit\"/>.\n    /// </summary>\n    public TimeSpan? LiveRecordLimit { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.TaskStartAt\"/>.\n    /// </summary>\n    public DateTime? TaskStartAt { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SkipMerge\"/>.\n    /// </summary>\n    public bool SkipMerge { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.BinaryMerge\"/>.\n    /// </summary>\n    public bool BinaryMerge { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.ForceAnsiConsole\"/>.\n    /// </summary>\n    public bool ForceAnsiConsole { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.NoAnsiColor\"/>.\n    /// </summary>\n    public bool NoAnsiColor { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.UseFFmpegConcatDemuxer\"/>.\n    /// </summary>\n    public bool UseFFmpegConcatDemuxer { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DelAfterDone\"/>.\n    /// </summary>\n    public bool DelAfterDone { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.AutoSubtitleFix\"/>.\n    /// </summary>\n    public bool AutoSubtitleFix { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.CheckSegmentsCount\"/>.\n    /// </summary>\n    public bool CheckSegmentsCount { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SkipDownload\"/>.\n    /// </summary>\n    public bool SkipDownload { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.WriteMetaJson\"/>.\n    /// </summary>\n    public bool WriteMetaJson { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.AppendUrlParams\"/>.\n    /// </summary>\n    public bool AppendUrlParams { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.MP4RealTimeDecryption\"/>.\n    /// </summary>\n    public bool MP4RealTimeDecryption { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.UseShakaPackager\"/>.\n    /// </summary>\n    [Obsolete(\"Use DecryptionEngine instead\")]\n    public bool UseShakaPackager { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DecryptionEngine\"/>.\n    /// </summary>\n    public DecryptEngine DecryptionEngine { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.MuxAfterDone\"/>.\n    /// </summary>\n    public bool MuxAfterDone { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.ConcurrentDownload\"/>.\n    /// </summary>\n    public bool ConcurrentDownload { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LiveRealTimeMerge\"/>.\n    /// </summary>\n    public bool LiveRealTimeMerge { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LiveKeepSegments\"/>.\n    /// </summary>\n    public bool LiveKeepSegments { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LivePerformAsVod\"/>.\n    /// </summary>\n    public bool LivePerformAsVod { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.UseSystemProxy\"/>.\n    /// </summary>\n    public bool UseSystemProxy { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SubtitleFormat\"/>.\n    /// </summary>\n    public SubtitleFormat SubtitleFormat { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.TmpDir\"/>.\n    /// </summary>\n    public string? TmpDir { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SaveDir\"/>.\n    /// </summary>\n    public string? SaveDir { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SaveName\"/>.\n    /// </summary>\n    public string? SaveName { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SavePattern\"/>.\n    /// </summary>\n    public string? SavePattern { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LogFilePath\"/>.\n    /// </summary>\n    public string? LogFilePath { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.UILanguage\"/>.\n    /// </summary>\n    public string? UILanguage { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DecryptionBinaryPath\"/>.\n    /// </summary>\n    public string? DecryptionBinaryPath { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.FFmpegBinaryPath\"/>.\n    /// </summary>\n    public string? FFmpegBinaryPath { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.MkvmergeBinaryPath\"/>.\n    /// </summary>\n    public string? MkvmergeBinaryPath { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.MuxImports\"/>.\n    /// </summary>\n    public List<OutputFile>? MuxImports { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.VideoFilter\"/>.\n    /// </summary>\n    public StreamFilter? VideoFilter { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DropVideoFilter\"/>.\n    /// </summary>\n    public StreamFilter? DropVideoFilter { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.AudioFilter\"/>.\n    /// </summary>\n    public StreamFilter? AudioFilter { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DropAudioFilter\"/>.\n    /// </summary>\n    public StreamFilter? DropAudioFilter { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.SubtitleFilter\"/>.\n    /// </summary>\n    public StreamFilter? SubtitleFilter { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.DropSubtitleFilter\"/>.\n    /// </summary>\n    public StreamFilter? DropSubtitleFilter { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.CustomHLSMethod\"/>.\n    /// </summary>\n    public EncryptMethod? CustomHLSMethod { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.CustomHLSKey\"/>.\n    /// </summary>\n    public byte[]? CustomHLSKey { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.CustomHLSIv\"/>.\n    /// </summary>\n    public byte[]? CustomHLSIv { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.CustomProxy\"/>.\n    /// </summary>\n    public WebProxy? CustomProxy { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.CustomRange\"/>.\n    /// </summary>\n    public CustomRange? CustomRange { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LiveWaitTime\"/>.\n    /// </summary>\n    public int? LiveWaitTime { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LiveTakeCount\"/>.\n    /// </summary>\n    public int LiveTakeCount { get; set; }\n    public MuxOptions? MuxOptions { get; set; }\n    // public bool LiveWriteHLS { get; set; } = true;\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LivePipeMux\"/>.\n    /// </summary>\n    public bool LivePipeMux { get; set; }\n    /// <summary>\n    /// See: <see cref=\"CommandInvoker.LiveFixVttByAudio\"/>.\n    /// </summary>\n    public bool LiveFixVttByAudio { get; set; }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Config/DownloaderConfig.cs",
    "content": "﻿using N_m3u8DL_RE.CommandLine;\n\nnamespace N_m3u8DL_RE.Config;\n\ninternal class DownloaderConfig\n{\n    public required MyOption MyOptions { get; set; }\n\n    /// <summary>\n    /// 前置阶段生成的文件夹名\n    /// </summary>\n    public required string DirPrefix { get; set; }\n    /// <summary>\n    /// 文件名模板\n    /// </summary>\n    public string? SavePattern { get; set; }\n    /// <summary>\n    /// 校验响应头的文件大小和实际大小\n    /// </summary>\n    public bool CheckContentLength { get; set; } = true;\n    /// <summary>\n    /// 请求头\n    /// </summary>\n    public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Config/EnvConfigKey.cs",
    "content": "namespace N_m3u8DL_RE.Config;\n\n/// <summary>\n/// 通过配置环境变量来实现更细节地控制某些逻辑\n/// </summary>\npublic static class EnvConfigKey\n{\n    /// <summary>\n    /// 当此值为1时, 在图形字幕处理逻辑中PNG生成后不再删除m4s文件\n    /// </summary>\n    public const string ReKeepImageSegments = \"RE_KEEP_IMAGE_SEGMENTS\";\n    \n    /// <summary>\n    /// 控制启用PipeMux时, 具体ffmpeg命令行\n    /// </summary>\n    public const string ReLivePipeOptions = \"RE_LIVE_PIPE_OPTIONS\";\n    \n    /// <summary>\n    /// 控制启用PipeMux时, 非Windows环境下命名管道文件的生成目录\n    /// </summary>\n    public const string ReLivePipeTmpDir = \"RE_LIVE_PIPE_TMP_DIR\";\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Crypto/AESUtil.cs",
    "content": "﻿using System.Security.Cryptography;\n\nnamespace N_m3u8DL_RE.Crypto;\n\ninternal static class AESUtil\n{\n    /// <summary>\n    /// AES-128解密，解密后原地替换文件\n    /// </summary>\n    /// <param name=\"filePath\"></param>\n    /// <param name=\"keyByte\"></param>\n    /// <param name=\"ivByte\"></param>\n    /// <param name=\"mode\"></param>\n    /// <param name=\"padding\"></param>\n    public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)\n    {\n        var fileBytes = File.ReadAllBytes(filePath);\n        var decrypted = AES128Decrypt(fileBytes, keyByte, ivByte, mode, padding);\n        File.WriteAllBytes(filePath, decrypted);\n    }\n\n    public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)\n    {\n        byte[] inBuff = encryptedBuff;\n\n        Aes dcpt = Aes.Create();\n        dcpt.BlockSize = 128;\n        dcpt.KeySize = 128;\n        dcpt.Key = keyByte;\n        dcpt.IV = ivByte;\n        dcpt.Mode = mode;\n        dcpt.Padding = padding;\n\n        ICryptoTransform cTransform = dcpt.CreateDecryptor();\n        byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length);\n        return resultArray;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Crypto/CSChaCha20.cs",
    "content": "﻿/*\n * Copyright (c) 2015, 2018 Scott Bennett\n *           (c) 2018-2021 Kaarlo Räihä\n *\n * Permission to use, copy, modify, and distribute this software for any\n * purpose with or without fee is hereby granted, provided that the above\n * copyright notice and this permission notice appear in all copies.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n */\n\nusing System;\nusing System.IO;\nusing System.Text;\nusing System.Threading.Tasks;\nusing System.Runtime.CompilerServices; // For MethodImplOptions.AggressiveInlining\n\nnamespace CSChaCha20\n{\n    /// <summary>\n    /// Class that can be used for ChaCha20 encryption / decryption\n    /// </summary>\n    public sealed class ChaCha20 : IDisposable\n    {\n        /// <summary>\n        /// Only allowed key lenght in bytes\n        /// </summary>\n        public const int allowedKeyLength = 32;\n\n        /// <summary>\n        /// Only allowed nonce lenght in bytes\n        /// </summary>\n        public const int allowedNonceLength = 12;\n\n        /// <summary>\n        /// How many bytes are processed per loop\n        /// </summary>\n        public const int processBytesAtTime = 64;\n\n        private const int stateLength = 16;\n\n        /// <summary>\n        /// The ChaCha20 state (aka \"context\")\n        /// </summary>\n        private readonly uint[] state = new uint[stateLength];\n\n        /// <summary>\n        /// Determines if the objects in this class have been disposed of. Set to true by the Dispose() method.\n        /// </summary>\n        private bool isDisposed = false;\n\n        /// <summary>\n        /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens.\n        /// </summary>\n        /// <remarks>\n        /// See <a href=\"https://tools.ietf.org/html/rfc7539#page-10\">ChaCha20 Spec Section 2.4</a> for a detailed description of the inputs.\n        /// </remarks>\n        /// <param name=\"key\">\n        /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers\n        /// </param>\n        /// <param name=\"nonce\">\n        /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers\n        /// </param>\n        /// <param name=\"counter\">\n        /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer\n        /// </param>\n        public ChaCha20(byte[] key, byte[] nonce, uint counter)\n        {\n            this.KeySetup(key);\n            this.IvSetup(nonce, counter);\n        }\n\n#if NET6_0_OR_GREATER\n\n\t\t/// <summary>\n\t\t/// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens.\n\t\t/// </summary>\n\t\t/// <remarks>\n\t\t/// See <a href=\"https://tools.ietf.org/html/rfc7539#page-10\">ChaCha20 Spec Section 2.4</a> for a detailed description of the inputs.\n\t\t/// </remarks>\n\t\t/// <param name=\"key\">A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers</param>\n\t\t/// <param name=\"nonce\">A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers</param>\n\t\t/// <param name=\"counter\">A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer</param>\n\t\tpublic ChaCha20(ReadOnlySpan<byte> key, ReadOnlySpan<byte> nonce, uint counter) \n\t\t{\n\t\t\tthis.KeySetup(key.ToArray());\n\t\t\tthis.IvSetup(nonce.ToArray(), counter);\n\t\t}\n\n#endif // NET6_0_OR_GREATER\n\n        /// <summary>\n        /// The ChaCha20 state (aka \"context\"). Read-Only.\n        /// </summary>\n        public uint[] State\n        {\n            get\n            {\n                return this.state;\n            }\n        }\n\n\n        // These are the same constants defined in the reference implementation.\n        // http://cr.yp.to/streamciphers/timings/estreambench/submissions/salsa20/chacha8/ref/chacha.c\n        private static readonly byte[] sigma = Encoding.ASCII.GetBytes(\"expand 32-byte k\");\n        private static readonly byte[] tau = Encoding.ASCII.GetBytes(\"expand 16-byte k\");\n\n        /// <summary>\n        /// Set up the ChaCha state with the given key. A 32-byte key is required and enforced.\n        /// </summary>\n        /// <param name=\"key\">\n        /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers\n        /// </param>\n        private void KeySetup(byte[] key)\n        {\n            if (key == null)\n            {\n                throw new ArgumentNullException(\"Key is null\");\n            }\n\n            if (key.Length != allowedKeyLength)\n            {\n                throw new ArgumentException($\"Key length must be {allowedKeyLength}. Actual: {key.Length}\");\n            }\n\n            state[4] = Util.U8To32Little(key, 0);\n            state[5] = Util.U8To32Little(key, 4);\n            state[6] = Util.U8To32Little(key, 8);\n            state[7] = Util.U8To32Little(key, 12);\n\n            byte[] constants = (key.Length == allowedKeyLength) ? sigma : tau;\n            int keyIndex = key.Length - 16;\n\n            state[8] = Util.U8To32Little(key, keyIndex + 0);\n            state[9] = Util.U8To32Little(key, keyIndex + 4);\n            state[10] = Util.U8To32Little(key, keyIndex + 8);\n            state[11] = Util.U8To32Little(key, keyIndex + 12);\n\n            state[0] = Util.U8To32Little(constants, 0);\n            state[1] = Util.U8To32Little(constants, 4);\n            state[2] = Util.U8To32Little(constants, 8);\n            state[3] = Util.U8To32Little(constants, 12);\n        }\n\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"nonce\">\n        /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers\n        /// </param>\n        /// <param name=\"counter\">\n        /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer\n        /// </param>\n        private void IvSetup(byte[] nonce, uint counter)\n        {\n            if (nonce == null)\n            {\n                // There has already been some state set up. Clear it before exiting.\n                Dispose();\n                throw new ArgumentNullException(\"Nonce is null\");\n            }\n\n            if (nonce.Length != allowedNonceLength)\n            {\n                // There has already been some state set up. Clear it before exiting.\n                Dispose();\n                throw new ArgumentException($\"Nonce length must be {allowedNonceLength}. Actual: {nonce.Length}\");\n            }\n\n            state[12] = counter;\n            state[13] = Util.U8To32Little(nonce, 0);\n            state[14] = Util.U8To32Little(nonce, 4);\n            state[15] = Util.U8To32Little(nonce, 8);\n        }\n\n\n        #region Encryption methods\n\n        /// <summary>\n        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"output\">Output byte array, must have enough bytes</param>\n        /// <param name=\"input\">Input byte array</param>\n        /// <param name=\"numBytes\">Number of bytes to encrypt</param>\n        public void EncryptBytes(byte[] output, byte[] input, int numBytes)\n        {\n            this.WorkBytes(output, input, numBytes);\n        }\n\n        /// <summary>\n        /// Encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)\n        /// </summary>\n        /// <param name=\"output\">Output stream</param>\n        /// <param name=\"input\">Input stream</param>\n        /// <param name=\"howManyBytesToProcessAtTime\">How many bytes to read and write at time, default is 1024</param>\n        public void EncryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)\n        {\n            this.WorkStreams(output, input, howManyBytesToProcessAtTime);\n        }\n\n        /// <summary>\n        /// Async encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)\n        /// </summary>\n        /// <param name=\"output\">Output stream</param>\n        /// <param name=\"input\">Input stream</param>\n        /// <param name=\"howManyBytesToProcessAtTime\">How many bytes to read and write at time, default is 1024</param>\n        public async Task EncryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)\n        {\n            await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime);\n        }\n\n        /// <summary>\n        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"output\">Output byte array, must have enough bytes</param>\n        /// <param name=\"input\">Input byte array</param>\n        public void EncryptBytes(byte[] output, byte[] input)\n        {\n            this.WorkBytes(output, input, input.Length);\n        }\n\n        /// <summary>\n        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"input\">Input byte array</param>\n        /// <param name=\"numBytes\">Number of bytes to encrypt</param>\n        /// <returns>Byte array that contains encrypted bytes</returns>\n        public byte[] EncryptBytes(byte[] input, int numBytes)\n        {\n            byte[] returnArray = new byte[numBytes];\n            this.WorkBytes(returnArray, input, numBytes);\n            return returnArray;\n        }\n\n        /// <summary>\n        /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"input\">Input byte array</param>\n        /// <returns>Byte array that contains encrypted bytes</returns>\n        public byte[] EncryptBytes(byte[] input)\n        {\n            byte[] returnArray = new byte[input.Length];\n            this.WorkBytes(returnArray, input, input.Length);\n            return returnArray;\n        }\n\n        /// <summary>\n        /// Encrypt string as UTF8 byte array, returns byte array that is allocated by method.\n        /// </summary>\n        /// <remarks>Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform</remarks>\n        /// <param name=\"input\">Input string</param>\n        /// <returns>Byte array that contains encrypted bytes</returns>\n        public byte[] EncryptString(string input)\n        {\n            byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(input);\n            byte[] returnArray = new byte[utf8Bytes.Length];\n\n            this.WorkBytes(returnArray, utf8Bytes, utf8Bytes.Length);\n            return returnArray;\n        }\n\n        #endregion // Encryption methods\n\n\n        #region // Decryption methods\n\n        /// <summary>\n        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to the output buffer.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"output\">Output byte array</param>\n        /// <param name=\"input\">Input byte array</param>\n        /// <param name=\"numBytes\">Number of bytes to decrypt</param>\n        public void DecryptBytes(byte[] output, byte[] input, int numBytes)\n        {\n            this.WorkBytes(output, input, numBytes);\n        }\n\n        /// <summary>\n        /// Decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)\n        /// </summary>\n        /// <param name=\"output\">Output stream</param>\n        /// <param name=\"input\">Input stream</param>\n        /// <param name=\"howManyBytesToProcessAtTime\">How many bytes to read and write at time, default is 1024</param>\n        public void DecryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)\n        {\n            this.WorkStreams(output, input, howManyBytesToProcessAtTime);\n        }\n\n        /// <summary>\n        /// Async decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output)\n        /// </summary>\n        /// <param name=\"output\">Output stream</param>\n        /// <param name=\"input\">Input stream</param>\n        /// <param name=\"howManyBytesToProcessAtTime\">How many bytes to read and write at time, default is 1024</param>\n        public async Task DecryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)\n        {\n            await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime);\n        }\n\n        /// <summary>\n        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"output\">Output byte array, must have enough bytes</param>\n        /// <param name=\"input\">Input byte array</param>\n        public void DecryptBytes(byte[] output, byte[] input)\n        {\n            WorkBytes(output, input, input.Length);\n        }\n\n        /// <summary>\n        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"input\">Input byte array</param>\n        /// <param name=\"numBytes\">Number of bytes to encrypt</param>\n        /// <returns>Byte array that contains decrypted bytes</returns>\n        public byte[] DecryptBytes(byte[] input, int numBytes)\n        {\n            byte[] returnArray = new byte[numBytes];\n            WorkBytes(returnArray, input, numBytes);\n            return returnArray;\n        }\n\n        /// <summary>\n        /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method.\n        /// </summary>\n        /// <remarks>Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method</remarks>\n        /// <param name=\"input\">Input byte array</param>\n        /// <returns>Byte array that contains decrypted bytes</returns>\n        public byte[] DecryptBytes(byte[] input)\n        {\n            byte[] returnArray = new byte[input.Length];\n            WorkBytes(returnArray, input, input.Length);\n            return returnArray;\n        }\n\n        /// <summary>\n        /// Decrypt UTF8 byte array to string.\n        /// </summary>\n        /// <remarks>Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform</remarks>\n        /// <param name=\"input\">Byte array</param>\n        /// <returns>Byte array that contains encrypted bytes</returns>\n        public string DecryptUTF8ByteArray(byte[] input)\n        {\n            byte[] tempArray = new byte[input.Length];\n\n            WorkBytes(tempArray, input, input.Length);\n            return System.Text.Encoding.UTF8.GetString(tempArray);\n        }\n\n        #endregion // Decryption methods\n\n        private void WorkStreams(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)\n        {\n            int readBytes;\n\n            byte[] inputBuffer = new byte[howManyBytesToProcessAtTime];\n            byte[] outputBuffer = new byte[howManyBytesToProcessAtTime];\n\n            while ((readBytes = input.Read(inputBuffer, 0, howManyBytesToProcessAtTime)) > 0)\n            {\n                // Encrypt or decrypt\n                WorkBytes(output: outputBuffer, input: inputBuffer, numBytes: readBytes);\n\n                // Write buffer\n                output.Write(outputBuffer, 0, readBytes);\n            }\n        }\n\n        private async Task WorkStreamsAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024)\n        {\n            byte[] readBytesBuffer = new byte[howManyBytesToProcessAtTime];\n            byte[] writeBytesBuffer = new byte[howManyBytesToProcessAtTime];\n            int howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime);\n\n            while (howManyBytesWereRead > 0)\n            {\n                // Encrypt or decrypt\n                WorkBytes(output: writeBytesBuffer, input: readBytesBuffer, numBytes: howManyBytesWereRead);\n\n                // Write\n                await output.WriteAsync(writeBytesBuffer, 0, howManyBytesWereRead);\n\n                // Read more\n                howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime);\n            }\n        }\n\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"output\">Output byte array</param>\n        /// <param name=\"input\">Input byte array</param>\n        /// <param name=\"numBytes\">How many bytes to process</param>\n        private void WorkBytes(byte[] output, byte[] input, int numBytes)\n        {\n            if (isDisposed)\n            {\n                throw new ObjectDisposedException(\"state\", \"The ChaCha state has been disposed\");\n            }\n\n            if (input == null)\n            {\n                throw new ArgumentNullException(\"input\", \"Input cannot be null\");\n            }\n\n            if (output == null)\n            {\n                throw new ArgumentNullException(\"output\", \"Output cannot be null\");\n            }\n\n            if (numBytes < 0 || numBytes > input.Length)\n            {\n                throw new ArgumentOutOfRangeException(\"numBytes\", \"The number of bytes to read must be between [0..input.Length]\");\n            }\n\n            if (output.Length < numBytes)\n            {\n                throw new ArgumentOutOfRangeException(\"output\", $\"Output byte array should be able to take at least {numBytes}\");\n            }\n\n            uint[] x = new uint[stateLength];    // Working buffer\n            byte[] tmp = new byte[processBytesAtTime];  // Temporary buffer\n            int offset = 0;\n\n            while (numBytes > 0)\n            {\n                // Copy state to working buffer\n                Buffer.BlockCopy(this.state, 0, x, 0, stateLength * sizeof(uint));\n\n                for (int i = 0; i < 10; i++)\n                {\n                    QuarterRound(x, 0, 4, 8, 12);\n                    QuarterRound(x, 1, 5, 9, 13);\n                    QuarterRound(x, 2, 6, 10, 14);\n                    QuarterRound(x, 3, 7, 11, 15);\n\n                    QuarterRound(x, 0, 5, 10, 15);\n                    QuarterRound(x, 1, 6, 11, 12);\n                    QuarterRound(x, 2, 7, 8, 13);\n                    QuarterRound(x, 3, 4, 9, 14);\n                }\n\n                for (int i = 0; i < stateLength; i++)\n                {\n                    Util.ToBytes(tmp, Util.Add(x[i], this.state[i]), 4 * i);\n                }\n\n                this.state[12] = Util.AddOne(state[12]);\n                if (this.state[12] <= 0)\n                {\n                    /* Stopping at 2^70 bytes per nonce is the user's responsibility */\n                    this.state[13] = Util.AddOne(state[13]);\n                }\n\n                // In case these are last bytes\n                if (numBytes <= processBytesAtTime)\n                {\n                    for (int i = 0; i < numBytes; i++)\n                    {\n                        output[i + offset] = (byte)(input[i + offset] ^ tmp[i]);\n                    }\n\n                    return;\n                }\n\n                for (int i = 0; i < processBytesAtTime; i++)\n                {\n                    output[i + offset] = (byte)(input[i + offset] ^ tmp[i]);\n                }\n\n                numBytes -= processBytesAtTime;\n                offset += processBytesAtTime;\n            }\n        }\n\n        /// <summary>\n        /// The ChaCha Quarter Round operation. It operates on four 32-bit unsigned integers within the given buffer at indices a, b, c, and d.\n        /// </summary>\n        /// <remarks>\n        /// 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.\n        /// See <a href=\"https://tools.ietf.org/html/rfc7539#page-4\">ChaCha20 Spec Sections 2.1 - 2.2</a>.\n        /// </remarks>\n        /// <param name=\"x\">A ChaCha state (vector). Must contain 16 elements.</param>\n        /// <param name=\"a\">Index of the first number</param>\n        /// <param name=\"b\">Index of the second number</param>\n        /// <param name=\"c\">Index of the third number</param>\n        /// <param name=\"d\">Index of the fourth number</param>\n        private static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d)\n        {\n            x[a] = Util.Add(x[a], x[b]);\n            x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 16);\n\n            x[c] = Util.Add(x[c], x[d]);\n            x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 12);\n\n            x[a] = Util.Add(x[a], x[b]);\n            x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 8);\n\n            x[c] = Util.Add(x[c], x[d]);\n            x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 7);\n        }\n\n        #region Destructor and Disposer\n\n        /// <summary>\n        /// Clear and dispose of the internal state. The finalizer is only called if Dispose() was never called on this cipher.\n        /// </summary>\n        ~ChaCha20()\n        {\n            Dispose(false);\n        }\n\n        /// <summary>\n        /// Clear and dispose of the internal state. Also request the GC not to call the finalizer, because all cleanup has been taken care of.\n        /// </summary>\n        public void Dispose()\n        {\n            Dispose(true);\n            /*\n\t\t\t * The Garbage Collector does not need to invoke the finalizer because Dispose(bool) has already done all the cleanup needed.\n\t\t\t */\n            GC.SuppressFinalize(this);\n        }\n\n        /// <summary>\n        /// This method should only be invoked from Dispose() or the finalizer. This handles the actual cleanup of the resources.\n        /// </summary>\n        /// <param name=\"disposing\">\n        /// Should be true if called by Dispose(); false if called by the finalizer\n        /// </param>\n        private void Dispose(bool disposing)\n        {\n            if (!isDisposed)\n            {\n                if (disposing)\n                {\n                    /* Cleanup managed objects by calling their Dispose() methods */\n                }\n\n                /* Cleanup any unmanaged objects here */\n                Array.Clear(state, 0, stateLength);\n            }\n\n            isDisposed = true;\n        }\n\n        #endregion // Destructor and Disposer\n    }\n\n    /// <summary>\n    /// Utilities that are used during compression\n    /// </summary>\n    public static class Util\n    {\n        /// <summary>\n        /// n-bit left rotation operation (towards the high bits) for 32-bit integers.\n        /// </summary>\n        /// <param name=\"v\"></param>\n        /// <param name=\"c\"></param>\n        /// <returns>The result of (v LEFTSHIFT c)</returns>\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        public static uint Rotate(uint v, int c)\n        {\n            unchecked\n            {\n                return (v << c) | (v >> (32 - c));\n            }\n        }\n\n        /// <summary>\n        /// Unchecked integer exclusive or (XOR) operation.\n        /// </summary>\n        /// <param name=\"v\"></param>\n        /// <param name=\"w\"></param>\n        /// <returns>The result of (v XOR w)</returns>\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        public static uint XOr(uint v, uint w)\n        {\n            return unchecked(v ^ w);\n        }\n\n        /// <summary>\n        /// Unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32.\n        /// </summary>\n        /// <remarks>\n        /// See <a href=\"https://tools.ietf.org/html/rfc7539#page-4\">ChaCha20 Spec Section 2.1</a>.\n        /// </remarks>\n        /// <param name=\"v\"></param>\n        /// <param name=\"w\"></param>\n        /// <returns>The result of (v + w) modulo 2^32</returns>\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        public static uint Add(uint v, uint w)\n        {\n            return unchecked(v + w);\n        }\n\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <remarks>\n        /// See <a href=\"https://tools.ietf.org/html/rfc7539#page-4\">ChaCha20 Spec Section 2.1</a>.\n        /// </remarks>\n        /// <param name=\"v\"></param>\n        /// <returns>The result of (v + 1) modulo 2^32</returns>\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        public static uint AddOne(uint v)\n        {\n            return unchecked(v + 1);\n        }\n\n        /// <summary>\n        /// Convert four bytes of the input buffer into an unsigned 32-bit integer, beginning at the inputOffset.\n        /// </summary>\n        /// <param name=\"p\"></param>\n        /// <param name=\"inputOffset\"></param>\n        /// <returns>An unsigned 32-bit integer</returns>\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        public static uint U8To32Little(byte[] p, int inputOffset)\n        {\n            unchecked\n            {\n                return ((uint)p[inputOffset]\n                    | ((uint)p[inputOffset + 1] << 8)\n                    | ((uint)p[inputOffset + 2] << 16)\n                    | ((uint)p[inputOffset + 3] << 24));\n            }\n        }\n\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"output\"></param>\n        /// <param name=\"input\"></param>\n        /// <param name=\"outputOffset\"></param>\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        public static void ToBytes(byte[] output, uint input, int outputOffset)\n        {\n            unchecked\n            {\n                output[outputOffset] = (byte)input;\n                output[outputOffset + 1] = (byte)(input >> 8);\n                output[outputOffset + 2] = (byte)(input >> 16);\n                output[outputOffset + 3] = (byte)(input >> 24);\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs",
    "content": "﻿using CSChaCha20;\n\nnamespace N_m3u8DL_RE.Crypto;\n\ninternal static class ChaCha20Util\n{\n    public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes)\n    {\n        if (keyBytes.Length != 32)\n            throw new Exception(\"Key must be 32 bytes!\");\n        if (nonceBytes.Length != 12 && nonceBytes.Length != 8)\n            throw new Exception(\"Key must be 12 or 8 bytes!\");\n        if (nonceBytes.Length == 8)\n            nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray();\n\n        var decStream = new MemoryStream();\n        using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff));\n        using (BinaryWriter writer = new BinaryWriter(decStream))\n            while (true)\n            {\n                var buffer = reader.ReadBytes(1024);\n                byte[] dec = new byte[buffer.Length];\n                if (buffer.Length > 0)\n                {\n                    ChaCha20 forDecrypting = new ChaCha20(keyBytes, nonceBytes, 0);\n                    forDecrypting.DecryptBytes(dec, buffer);\n                    writer.Write(dec, 0, dec.Length);\n                }\n                else\n                {\n                    break;\n                }\n            }\n\n        return decStream.ToArray();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Directory.Build.props",
    "content": "<Project>\n\n    <PropertyGroup>\n      <IlcOptimizationPreference>Speed</IlcOptimizationPreference>\n      <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>\n      <StaticallyLinked Condition=\"$(RuntimeIdentifier.StartsWith('win'))\">true</StaticallyLinked>\n      <TrimMode>full</TrimMode>\n      <TrimmerDefaultAction>link</TrimmerDefaultAction>\n      <IlcTrimMetadata>true</IlcTrimMetadata>\n      <IlcGenerateStackTraceData>true</IlcGenerateStackTraceData>\n      <SatelliteResourceLanguages>zh-CN;zh-TW;en-US</SatelliteResourceLanguages>\n      <PublishAot>true</PublishAot>\n      <StripSymbols>true</StripSymbols>\n      <ObjCopyName Condition=\"'$(RuntimeIdentifier)' == 'linux-arm64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'\">aarch64-linux-gnu-objcopy</ObjCopyName>\n    </PropertyGroup>\n\n  <ItemGroup Condition=\"'$(RuntimeIdentifier)' == 'linux-musl-arm64' or '$(RuntimeIdentifier)' == 'linux-musl-x64'\">\n    <!-- 导入libc -->\n    <DirectPInvoke Include=\"libc\" />\n  </ItemGroup>\n\n  <PropertyGroup Condition=\"'$(RuntimeIdentifier)' == 'linux-musl-arm64' or '$(RuntimeIdentifier)' == 'linux-musl-x64'\">\n    <!-- 设置为纯静态应用 -->\n    <StaticExecutable>true</StaticExecutable>\n    <!-- 静态链接openssl -->\n    <StaticOpenSslLinking>true</StaticOpenSslLinking>\n    <!-- 去除国际化支持 -->\n    <InvariantGlobalization>true</InvariantGlobalization>\n  </PropertyGroup>\n\n    <!--<ItemGroup Condition=\"'$(PublishAot)' == 'true' and '$(RuntimeIdentifier)' != 'win-arm64' and '$(RuntimeIdentifier)' != 'linux-arm64' and '$(RuntimeIdentifier)' != 'osx-arm64' and '$(RuntimeIdentifier)' != 'osx-x64'\">\n        <PackageReference Include=\"PublishAotCompressed\" Version=\"1.0.3\" />\n    </ItemGroup>-->\n\n</Project>\n"
  },
  {
    "path": "src/N_m3u8DL-RE/DownloadManager/HTTPLiveRecordManager.cs",
    "content": "﻿using N_m3u8DL_RE.Column;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Config;\nusing N_m3u8DL_RE.Downloader;\nusing N_m3u8DL_RE.Entity;\nusing N_m3u8DL_RE.Parser;\nusing N_m3u8DL_RE.Util;\nusing Spectre.Console;\nusing System.Collections.Concurrent;\nusing System.Text;\n\nnamespace N_m3u8DL_RE.DownloadManager;\n\ninternal class HTTPLiveRecordManager\n{\n    private static HttpClient HttpClient = new();\n    \n    IDownloader Downloader;\n    DownloaderConfig DownloaderConfig;\n    StreamExtractor StreamExtractor;\n    List<StreamSpec> SelectedSteams;\n    List<OutputFile> OutputFiles = [];\n    DateTime NowDateTime;\n    DateTime? PublishDateTime;\n    bool STOP_FLAG = false;\n    bool READ_IFO = false;\n    ConcurrentDictionary<int, int> RecordingDurDic = new(); // 已录制时长\n    ConcurrentDictionary<int, double> RecordingSizeDic = new(); // 已录制大小\n    CancellationTokenSource CancellationTokenSource = new(); // 取消Wait\n    List<byte> InfoBuffer = new List<byte>(188 * 5000); // 5000个分包中解析信息，没有就算了\n\n    public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)\n    {\n        this.DownloaderConfig = downloaderConfig;\n        Downloader = new SimpleDownloader(DownloaderConfig);\n        NowDateTime = DateTime.Now;\n        PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;\n        StreamExtractor = streamExtractor;\n        SelectedSteams = selectedSteams;\n    }\n\n    private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)\n    {\n        task.MaxValue = 1;\n        task.StartTask();\n\n        var name = streamSpec.ToShortString();\n        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}\";\n        var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;\n\n        // Use SavePattern if provided, otherwise use SaveName or dirName\n        var saveName = dirName;\n        if (!string.IsNullOrWhiteSpace(DownloaderConfig.MyOptions.SavePattern))\n        {\n            saveName = OtherUtil.FormatSavePattern(DownloaderConfig.MyOptions.SavePattern, streamSpec, DownloaderConfig.MyOptions.SaveName, task.Id);\n        }\n        else if (DownloaderConfig.MyOptions.SaveName != null)\n        {\n            saveName = $\"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}\".TrimEnd('.');\n        }\n\n        Logger.Debug($\"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}\");\n\n        // 创建文件夹\n        if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);\n\n        using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url));\n        request.Headers.ConnectionClose = false;\n        foreach (var item in DownloaderConfig.Headers)\n        {\n            request.Headers.TryAddWithoutValidation(item.Key, item.Value);\n        }\n        Logger.Debug(request.Headers.ToString());\n\n        using var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationTokenSource.Token);\n        response.EnsureSuccessStatusCode();\n\n        var output = Path.Combine(saveDir, saveName + \".ts\");\n        using var stream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);\n        using var responseStream = await response.Content.ReadAsStreamAsync(CancellationTokenSource.Token);\n        var buffer = new byte[16 * 1024];\n        var size = 0;\n\n        // 计时器\n        _ = TimeCounterAsync();\n        // 读取INFO\n        _ = ReadInfoAsync();\n\n        try\n        {\n            while ((size = await responseStream.ReadAsync(buffer, CancellationTokenSource.Token)) > 0)\n            {\n                if (!READ_IFO && InfoBuffer.Count < 188 * 5000)\n                {\n                    InfoBuffer.AddRange(buffer);\n                }\n                speedContainer.Add(size);\n                RecordingSizeDic[task.Id] += size;\n                await stream.WriteAsync(buffer, 0, size);\n            }\n        }\n        catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)\n        {\n            ;\n        }\n\n        Logger.InfoMarkUp(\"File Size: \" + GlobalUtil.FormatFileSize(RecordingSizeDic[task.Id]));\n\n        return true;\n    }\n\n    public async Task ReadInfoAsync()\n    {\n        while (!STOP_FLAG && !READ_IFO)\n        {\n            await Task.Delay(200);\n            if (InfoBuffer.Count < 188 * 5000) continue;\n\n            ushort ConvertToUint16(IEnumerable<byte> bytes)\n            {\n                if (BitConverter.IsLittleEndian)\n                    bytes = bytes.Reverse();\n                return BitConverter.ToUInt16(bytes.ToArray());\n            }\n\n            var data = InfoBuffer.ToArray();\n            var programId = \"\";\n            var serviceProvider = \"\";\n            var serviceName = \"\";\n            for (int i = 0; i < data.Length; i++)\n            {\n                if (data[i] == 0x47 && (i + 188) < data.Length && data[i + 188] == 0x47)\n                {\n                    var tsData = data.Skip(i).Take(188);\n                    var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0);\n                    var pid = (tsHeaderInt & 0x1fff00) >> 8;\n                    var tsPayload = tsData.Skip(4);\n                    // PAT\n                    if (pid == 0x0000)\n                    {\n                        programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString();\n                    }\n                    // SDT, BAT, ST\n                    else if (pid == 0x0011)\n                    {\n                        var tableId = (int)tsPayload.Skip(1).First();\n                        // Current TS Info\n                        if (tableId == 0x42)\n                        {\n                            var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff;\n                            var sectionData = tsPayload.Skip(4).Take(sectionLength);\n                            var dscripData = sectionData.Skip(8);\n                            var descriptorsLoopLength = (ConvertToUint16(dscripData.Skip(3).Take(2))) & 0xfff;\n                            var descriptorsData = dscripData.Skip(5).Take(descriptorsLoopLength);\n                            var serviceProviderLength = (int)descriptorsData.Skip(3).First();\n                            serviceProvider = Encoding.UTF8.GetString(descriptorsData.Skip(4).Take(serviceProviderLength).ToArray());\n                            var serviceNameLength = (int)descriptorsData.Skip(4 + serviceProviderLength).First();\n                            serviceName = Encoding.UTF8.GetString(descriptorsData.Skip(5 + serviceProviderLength).Take(serviceNameLength).ToArray());\n                        }\n                    }\n                    if (programId != \"\" && (serviceName != \"\" || serviceProvider != \"\"))\n                        break;\n                }\n            }\n\n            if (!string.IsNullOrEmpty(programId))\n            {\n                Logger.InfoMarkUp($\"Program Id: [cyan]{programId.EscapeMarkup()}[/]\");\n                if (!string.IsNullOrEmpty(serviceName)) Logger.InfoMarkUp($\"Service Name: [cyan]{serviceName.EscapeMarkup()}[/]\");\n                if (!string.IsNullOrEmpty(serviceProvider)) Logger.InfoMarkUp($\"Service Provider: [cyan]{serviceProvider.EscapeMarkup()}[/]\");\n                READ_IFO = true;\n            }\n        }\n    }\n\n    public async Task TimeCounterAsync()\n    {\n        while (!STOP_FLAG)\n        {\n            await Task.Delay(1000);\n            RecordingDurDic[0]++;\n\n            // 检测时长限制\n            if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds))\n            {\n                Logger.WarnMarkUp($\"[darkorange3_1]{ResString.liveLimitReached}[/]\");\n                STOP_FLAG = true;\n                CancellationTokenSource.Cancel();\n            }\n        }\n    }\n\n    public async Task<bool> StartRecordAsync()\n    {\n        ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算\n        ConcurrentDictionary<StreamSpec, bool?> Results = new();\n\n        var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);\n        progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;\n\n        // 进度条的列定义\n        var progressColumns = new ProgressColumn[]\n        {\n            new TaskDescriptionColumn() { Alignment = Justify.Left },\n            new RecordingDurationColumn(RecordingDurDic), // 时长显示\n            new RecordingSizeColumn(RecordingSizeDic), // 大小显示\n            new RecordingStatusColumn(),\n            new DownloadSpeedColumn(SpeedContainerDic), // 速度计算\n            new SpinnerColumn(),\n        };\n        if (DownloaderConfig.MyOptions.NoAnsiColor)\n        {\n            progressColumns = progressColumns.SkipLast(1).ToArray();\n        }\n        progress.Columns(progressColumns);\n\n        await progress.StartAsync(async ctx =>\n        {\n            // 创建任务\n            var dic = SelectedSteams.Select(item =>\n            {\n                var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0);\n                SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算\n                RecordingDurDic[task.Id] = 0;\n                RecordingSizeDic[task.Id] = 0;\n                return (item, task);\n            }).ToDictionary(item => item.item, item => item.task);\n\n            DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;\n            var limit = DownloaderConfig.MyOptions.LiveRecordLimit;\n            if (limit != TimeSpan.MaxValue)\n                Logger.WarnMarkUp($\"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]\");\n            // 录制直播时，用户选了几个流就并发录几个\n            var options = new ParallelOptions()\n            {\n                MaxDegreeOfParallelism = SelectedSteams.Count\n            };\n            // 并发下载\n            await Parallel.ForEachAsync(dic, options, async (kp, _) =>\n            {\n                var task = kp.Value;\n                var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);\n                Results[kp.Key] = await consumerTask;\n            });\n        });\n\n        var success = Results.Values.All(v => v == true);\n\n        return success;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/DownloadManager/SimpleDownloadManager.cs",
    "content": "﻿using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Column;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Config;\nusing N_m3u8DL_RE.Downloader;\nusing N_m3u8DL_RE.Entity;\nusing N_m3u8DL_RE.Parser;\nusing N_m3u8DL_RE.Parser.Mp4;\nusing N_m3u8DL_RE.Util;\nusing Spectre.Console;\nusing System.Collections.Concurrent;\nusing System.Text;\nusing N_m3u8DL_RE.Enum;\n\nnamespace N_m3u8DL_RE.DownloadManager;\n\ninternal class SimpleDownloadManager\n{\n    IDownloader Downloader;\n    DownloaderConfig DownloaderConfig;\n    StreamExtractor StreamExtractor;\n    List<StreamSpec> SelectedSteams;\n    List<OutputFile> OutputFiles = [];\n\n    public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) \n    { \n        this.DownloaderConfig = downloaderConfig;\n        this.SelectedSteams = selectedSteams;\n        this.StreamExtractor = streamExtractor;\n        Downloader = new SimpleDownloader(DownloaderConfig);\n    }\n\n    // 从文件读取KEY\n    private async Task SearchKeyAsync(string? currentKID)\n    {\n        var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID);\n        if (_key != null)\n        {\n            if (DownloaderConfig.MyOptions.Keys == null)\n                DownloaderConfig.MyOptions.Keys = [_key];\n            else\n                DownloaderConfig.MyOptions.Keys = [..DownloaderConfig.MyOptions.Keys, _key];\n        }\n    }\n\n    private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)\n    {\n        if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))\n        {\n            DownloaderConfig.MyOptions.BinaryMerge = true;\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge2}[/]\");\n        }\n\n        if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))\n        {\n            DownloaderConfig.MyOptions.MuxAfterDone = false;\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge5}[/]\");\n        }\n\n        if (mediainfos.Where(m => m.Type == \"Audio\").All(m => m.BaseInfo!.Contains(\"aac\")))\n        {\n            useAACFilter = true;\n        }\n\n        if (mediainfos.All(m => m.Type == \"Audio\"))\n        {\n            streamSpec.MediaType = MediaType.AUDIO;\n        }\n        else if (mediainfos.All(m => m.Type == \"Subtitle\"))\n        {\n            streamSpec.MediaType = MediaType.SUBTITLES;\n            if (streamSpec.Extension is null or \"ts\")\n                streamSpec.Extension = \"vtt\";\n        }\n    }\n\n    private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)\n    {\n        speedContainer.ResetVars();\n        bool useAACFilter = false; // ffmpeg合并flag\n        List<Mediainfo> mediaInfos = [];\n        ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();\n\n        var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments);\n        if (segments == null || !segments.Any()) return false;\n        // 单分段尝试切片并行下载\n        if (segments.Count() == 1)\n        {\n            var splitSegments = await LargeSingleFileSplitUtil.SplitUrlAsync(segments.First(), DownloaderConfig.Headers);\n            if (splitSegments != null)\n            {\n                segments = splitSegments;\n                Logger.WarnMarkUp($\"[darkorange3_1]{ResString.singleFileSplitWarn}[/]\");\n                if (DownloaderConfig.MyOptions.MP4RealTimeDecryption)\n                {\n                    DownloaderConfig.MyOptions.MP4RealTimeDecryption = false;\n                    Logger.WarnMarkUp($\"[darkorange3_1]{ResString.singleFileRealtimeDecryptWarn}[/]\");\n                }\n            }\n            else speedContainer.SingleSegment = true;\n        }\n\n        var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO;\n        var dirName = $\"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? \"\", \"-\")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}\";\n        var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName);\n        var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;\n\n        // Use SavePattern if provided, otherwise use SaveName or dirName\n        var saveName = dirName;\n        if (!string.IsNullOrWhiteSpace(DownloaderConfig.MyOptions.SavePattern))\n        {\n            saveName = OtherUtil.FormatSavePattern(DownloaderConfig.MyOptions.SavePattern, streamSpec, DownloaderConfig.MyOptions.SaveName, task.Id);\n        }\n        else if (DownloaderConfig.MyOptions.SaveName != null)\n        {\n            saveName = $\"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}\".TrimEnd('.');\n        }\n        var headers = DownloaderConfig.Headers;\n\n        var decryptionBinaryPath = DownloaderConfig.MyOptions.DecryptionBinaryPath!;\n        var decryptEngine = DownloaderConfig.MyOptions.DecryptionEngine;\n        var mp4InitFile = \"\";\n        var currentKID = \"\";\n        var readInfo = false; // 是否读取过\n        var mp4Info = new ParsedMP4Info();\n\n        // 用户自定义范围导致被跳过的时长 计算字幕偏移使用\n        var skippedDur = streamSpec.SkippedDuration ?? 0d;\n\n        Logger.Debug($\"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}\");\n\n        // 创建文件夹\n        if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);\n        if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);\n\n        var totalCount = segments.Count();\n        if (streamSpec.Playlist?.MediaInit != null)\n        {\n            totalCount++;\n        }\n\n        task.MaxValue = totalCount;\n        task.StartTask();\n\n        // 开始下载\n        Logger.InfoMarkUp(ResString.startDownloading + streamSpec.ToShortString());\n\n        // 对于CENC，全部自动开启二进制合并\n        if (!DownloaderConfig.MyOptions.BinaryMerge && totalCount >= 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method == Common.Enum.EncryptMethod.CENC)\n        {\n            DownloaderConfig.MyOptions.BinaryMerge = true;\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge4}[/]\");\n        }\n\n        // 下载init\n        if (streamSpec.Playlist?.MediaInit != null)\n        {\n            // 对于fMP4，自动开启二进制合并\n            if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES)\n            {\n                DownloaderConfig.MyOptions.BinaryMerge = true;\n                Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge}[/]\");\n            }\n\n            var path = Path.Combine(tmpDir, \"_init.mp4.tmp\");\n            var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);\n            FileDic[streamSpec.Playlist.MediaInit] = result;\n            if (result is not { Success: true })\n            {\n                throw new Exception(\"Download init file failed!\");\n            }\n            mp4InitFile = result.ActualFilePath;\n            task.Increment(1);\n\n            // 读取mp4信息\n            if (result is { Success: true }) \n            {\n                mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath);\n                currentKID = mp4Info.KID;\n                // try shaka packager, which can handle WebM\n                if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {\n                    currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, decryptionBinaryPath);\n                }\n                // 从文件读取KEY\n                await SearchKeyAsync(currentKID);\n                // 实时解密\n                if ((streamSpec.Playlist.MediaInit.IsEncrypted || !string.IsNullOrEmpty(currentKID)) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS)\n                {\n                    var enc = result.ActualFilePath;\n                    var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                    var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM);\n                    if (dResult)\n                    {\n                        FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec;\n                    }\n                }\n                // ffmpeg读取信息\n                if (!readInfo)\n                {\n                    Logger.WarnMarkUp(ResString.readingInfo);\n                    mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath);\n                    mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));\n                    ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter);\n                    readInfo = true;\n                }\n            }\n        }\n\n        // 计算填零个数\n        var pad = \"0\".PadLeft(segments.Count().ToString().Length, '0');\n\n        // 下载第一个分片\n        if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS)\n        {\n            var seg = segments.First();\n            segments = segments.Skip(1);\n\n            var index = seg.Index;\n            var path = Path.Combine(tmpDir, index.ToString(pad) + $\".{streamSpec.Extension ?? \"clip\"}.tmp\");\n            var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);\n            FileDic[seg] = result;\n            if (result is not { Success: true })\n            {\n                throw new Exception(\"Download first segment failed!\");\n            }\n            task.Increment(1);\n            if (result is { Success: true })\n            {\n                // 修复MSS init\n                if (StreamExtractor.ExtractorType == ExtractorType.MSS)\n                {\n                    var processor = new MSSMoovProcessor(streamSpec);\n                    var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath));\n                    await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header);\n                    if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))\n                    {\n                        // 需要重新解密init\n                        var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath;\n                        var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                        var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);\n                        if (dResult)\n                        {\n                            FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec;\n                        }\n                    }\n                }\n                // 读取init信息\n                if (string.IsNullOrEmpty(currentKID))\n                {\n                    currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;\n                }\n                // try shaka packager, which can handle WebM\n                if (string.IsNullOrEmpty(currentKID) &&  DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {\n                    currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, decryptionBinaryPath);\n                }\n                // 从文件读取KEY\n                await SearchKeyAsync(currentKID);\n                // 实时解密\n                if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))\n                {\n                    var enc = result.ActualFilePath;\n                    var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                    mp4Info = MP4DecryptUtil.GetMP4Info(enc);\n                    var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM);\n                    if (dResult)\n                    {\n                        File.Delete(enc);\n                        result.ActualFilePath = dec;\n                    }\n                }\n                if (!readInfo)\n                {\n                    // ffmpeg读取信息\n                    Logger.WarnMarkUp(ResString.readingInfo);\n                    mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath);\n                    mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));\n                    ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter);\n                    readInfo = true;\n                }\n            }\n        }\n\n        // 开始下载\n        var options = new ParallelOptions()\n        {\n            MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount\n        };\n        await Parallel.ForEachAsync(segments, options, async (seg, _) =>\n        {\n            var index = seg.Index;\n            var path = Path.Combine(tmpDir, index.ToString(pad) + $\".{streamSpec.Extension ?? \"clip\"}.tmp\");\n            var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);\n            FileDic[seg] = result;\n            if (result is { Success: true })\n                task.Increment(1);\n            // 实时解密\n            if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result is { Success: true } && !string.IsNullOrEmpty(currentKID)) \n            {\n                var enc = result.ActualFilePath;\n                var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                mp4Info = MP4DecryptUtil.GetMP4Info(enc);\n                var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM);\n                if (dResult)\n                {\n                    File.Delete(enc);\n                    result.ActualFilePath = dec;\n                }\n            }\n        });\n\n        // 修改输出后缀\n        var outputExt = \".\" + streamSpec.Extension;\n        if (streamSpec.Extension == null) outputExt = \".ts\";\n        else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: \"m4s\" or \"mp4\" }) outputExt = \".m4a\";\n        else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension is \"m4s\" or \"mp4\") outputExt = \".mp4\";\n\n        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES)\n        {\n            outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? \".srt\" : \".vtt\";\n        }\n        var output = Path.Combine(saveDir, saveName + outputExt);\n\n        // 检测目标文件是否存在，使用智能重命名\n        var finalOutput = OtherUtil.HandleFileCollision(output, streamSpec);\n        if (finalOutput != output)\n        {\n            Logger.WarnMarkUp($\"{Path.GetFileName(output)} => {Path.GetFileName(finalOutput)}\");\n            output = finalOutput;\n        }\n\n        if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, Keys.Length: > 0 } && mp4InitFile != \"\")\n        {\n            File.Delete(mp4InitFile);\n            // shaka/ffmpeg实时解密不需要init文件用于合并\n            if (decryptEngine != DecryptEngine.MP4DECRYPT)\n            {\n                FileDic!.Remove(streamSpec.Playlist!.MediaInit, out _);\n            }\n        }\n\n        // 校验分片数量\n        if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null))\n        {\n            Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Count(s => s != null));\n            return false;\n        }\n\n        // 移除无效片段\n        var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key);\n        foreach (var badKey in badKeys)\n        {\n            FileDic!.Remove(badKey, out _);\n        }\n\n        // 校验完整性\n        if (DownloaderConfig.CheckContentLength && FileDic.Values.Any(a => a!.Success == false)) \n        {\n            return false;\n        }\n\n        // 自动修复VTT raw字幕\n        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains(\"vtt\")) \n        {\n            Logger.WarnMarkUp(ResString.fixingVTT);\n            // 排序字幕并修正时间戳\n            bool first = true;\n            var finalVtt = new WebVttSub();\n            var keys = FileDic.Keys.OrderBy(k => k.Index);\n            foreach (var seg in keys)\n            {\n                var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath);\n                var vtt = WebVttSub.Parse(vttContent);\n                // 手动计算MPEGTS\n                if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                {\n                    vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));\n                }\n                if (first) { finalVtt = vtt; first = false; }\n                else finalVtt.AddCuesFromOne(vtt);\n            }\n            // 写出字幕\n            var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();\n            foreach (var item in files) File.Delete(item);\n            FileDic.Clear();\n            var index = 0;\n            var path = Path.Combine(tmpDir, index.ToString(pad) + \".fix.vtt\");\n            // 设置字幕偏移\n            finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));\n            var subContentFixed = finalVtt.ToVtt();\n            // 转换字幕格式\n            if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)\n            {\n                path = Path.ChangeExtension(path, \".srt\");\n                subContentFixed = finalVtt.ToSrt();\n            }\n            await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);\n            FileDic[keys.First()] = new DownloadResult()\n            {\n                ActualContentLength = subContentFixed.Length,\n                ActualFilePath = path\n            };\n        }\n\n        // 自动修复VTT mp4字幕\n        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES\n                                                       && streamSpec.Codecs != \"stpp\" && streamSpec.Extension != null && streamSpec.Extension.Contains(\"m4s\"))\n        {\n            var initFile = FileDic.Values.FirstOrDefault(v => Path.GetFileName(v!.ActualFilePath).StartsWith(\"_init\"));\n            var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);\n            var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);\n            if (sawVtt)\n            {\n                Logger.WarnMarkUp(ResString.fixingVTTmp4);\n                var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(\".m4s\")).ToArray();\n                var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale);\n                // 写出字幕\n                var firstKey = FileDic.Keys.First();\n                var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();\n                foreach (var item in files) File.Delete(item);\n                FileDic.Clear();\n                var index = 0;\n                var path = Path.Combine(tmpDir, index.ToString(pad) + \".fix.vtt\");\n                // 设置字幕偏移\n                finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));\n                var subContentFixed = finalVtt.ToVtt();\n                // 转换字幕格式\n                if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)\n                {\n                    path = Path.ChangeExtension(path, \".srt\");\n                    subContentFixed = finalVtt.ToSrt();\n                }\n                await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);\n                FileDic[firstKey] = new DownloadResult()\n                {\n                    ActualContentLength = subContentFixed.Length,\n                    ActualFilePath = path\n                };\n            }\n        }\n\n        // 自动修复TTML raw字幕\n        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains(\"ttml\"))\n        {\n            Logger.WarnMarkUp(ResString.fixingTTML);\n            var first = true;\n            var finalVtt = new WebVttSub();\n            var keys = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Key);\n            foreach (var seg in keys)\n            {\n                var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0);\n                // 手动计算MPEGTS\n                if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                {\n                    vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));\n                }\n                if (first) { finalVtt = vtt; first = false; }\n                else finalVtt.AddCuesFromOne(vtt);\n            }\n            // 写出字幕\n            var firstKey = FileDic.Keys.First();\n            var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();\n\n            // 处理图形字幕\n            await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir);\n\n            var keepSegments = OtherUtil.GetEnvironmentVariable(EnvConfigKey.ReKeepImageSegments);\n            if (keepSegments != \"1\")\n                foreach (var item in files) File.Delete(item);\n            FileDic.Clear();\n            var index = 0;\n            var path = Path.Combine(tmpDir, index.ToString(pad) + \".fix.vtt\");\n            // 设置字幕偏移\n            finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));\n            var subContentFixed = finalVtt.ToVtt();\n            // 转换字幕格式\n            if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)\n            {\n                path = Path.ChangeExtension(path, \".srt\");\n                subContentFixed = finalVtt.ToSrt();\n            }\n            await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);\n            FileDic[firstKey] = new DownloadResult()\n            {\n                ActualContentLength = subContentFixed.Length,\n                ActualFilePath = path\n            };\n        }\n\n        // 自动修复TTML mp4字幕\n        if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains(\"m4s\")\n            && streamSpec.Codecs != null && streamSpec.Codecs.Contains(\"stpp\")) \n        {\n            Logger.WarnMarkUp(ResString.fixingTTMLmp4);\n            // sawTtml暂时不判断\n            // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith(\"_init\")).FirstOrDefault();\n            // var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);\n            // var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);\n            var first = true;\n            var finalVtt = new WebVttSub();\n            var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(\".m4s\")).Select(s => s.Key);\n            foreach (var seg in keys)\n            {\n                var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0);\n                // 手动计算MPEGTS\n                if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                {\n                    vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));\n                }\n                if (first) { finalVtt = vtt; first = false; }\n                else finalVtt.AddCuesFromOne(vtt);\n            }\n\n            // 写出字幕\n            var firstKey = FileDic.Keys.First();\n            var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();\n\n            // 处理图形字幕\n            await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir);\n\n            var keepSegments = OtherUtil.GetEnvironmentVariable(EnvConfigKey.ReKeepImageSegments);\n            if (keepSegments != \"1\")\n                foreach (var item in files) File.Delete(item);\n            FileDic.Clear();\n            var index = 0;\n            var path = Path.Combine(tmpDir, index.ToString(pad) + \".fix.vtt\");\n            // 设置字幕偏移\n            finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));\n            var subContentFixed = finalVtt.ToVtt();\n            // 转换字幕格式\n            if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)\n            {\n                path = Path.ChangeExtension(path, \".srt\");\n                subContentFixed = finalVtt.ToSrt();\n            }\n            await File.WriteAllTextAsync(path, subContentFixed, Encoding.UTF8);\n            FileDic[firstKey] = new DownloadResult()\n            {\n                ActualContentLength = subContentFixed.Length,\n                ActualFilePath = path\n            };\n        }\n\n        bool mergeSuccess = false;\n        // 合并\n        if (!DownloaderConfig.MyOptions.SkipMerge)\n        {\n            // 字幕也使用二进制合并\n            if (DownloaderConfig.MyOptions.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES)\n            {\n                Logger.InfoMarkUp(ResString.binaryMerge);\n                var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();\n                MergeUtil.CombineMultipleFilesIntoSingleFile(files, output);\n                mergeSuccess = true;\n            }\n            else\n            {\n                // ffmpeg合并\n                var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();\n                Logger.InfoMarkUp(ResString.ffmpegMerge);\n                var ext = streamSpec.MediaType == MediaType.AUDIO ? \"m4a\" : \"mp4\";\n                var ffOut = Path.Combine(Path.GetDirectoryName(output)!, Path.GetFileNameWithoutExtension(output) + $\".{ext}\");\n                // 检测目标文件是否存在，使用智能重命名\n                var finalFfOut = OtherUtil.HandleFileCollision(ffOut, streamSpec);\n                if (finalFfOut != ffOut)\n                {\n                    Logger.WarnMarkUp($\"{Path.GetFileName(ffOut)} => {Path.GetFileName(finalFfOut)}\");\n                    ffOut = finalFfOut;\n                }\n                // 大于1800分片，需要分步骤合并\n                if (files.Length >= 1800)\n                {\n                    Logger.WarnMarkUp(ResString.partMerge);\n                    files = MergeUtil.PartialCombineMultipleFiles(files);\n                    FileDic.Clear();\n                    foreach (var item in files)\n                    {\n                        FileDic[new MediaSegment() { Url = item }] = new DownloadResult()\n                        {\n                            ActualFilePath = item\n                        };\n                    }\n                }\n                mergeSuccess = MergeUtil.MergeByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, files, Path.ChangeExtension(ffOut, null), ext, useAACFilter, writeDate: !DownloaderConfig.MyOptions.NoDateInfo, useConcatDemuxer: DownloaderConfig.MyOptions.UseFFmpegConcatDemuxer);\n                if (mergeSuccess) output = ffOut;\n            }\n        }\n\n        // 删除临时文件夹\n        if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && mergeSuccess)\n        {\n            var files = FileDic.Values.Select(v => v!.ActualFilePath);\n            foreach (var file in files)\n            {\n                File.Delete(file);\n            }\n            OtherUtil.SafeDeleteDir(tmpDir);\n        }\n\n        // 重新读取init信息\n        if (mergeSuccess && totalCount >= 1 && string.IsNullOrEmpty(currentKID) && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE)\n        {\n            currentKID = MP4DecryptUtil.GetMP4Info(output).KID;\n            // try shaka packager, which can handle WebM\n            if (string.IsNullOrEmpty(currentKID) &&  DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {\n                currentKID = MP4DecryptUtil.ReadInitShaka(output, decryptionBinaryPath);\n            }\n            // 从文件读取KEY\n            await SearchKeyAsync(currentKID);\n        }\n\n        // 调用mp4decrypt解密\n        if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions is { MP4RealTimeDecryption: false, Keys.Length: > 0 })\n        {\n            var enc = output;\n            var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n            mp4Info = MP4DecryptUtil.GetMP4Info(enc);\n            Logger.InfoMarkUp($\"[grey]Decrypting using {decryptEngine}...[/]\");\n            var result = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM);\n            if (result)\n            {\n                File.Delete(enc);\n                File.Move(dec, enc);\n            }\n        }\n\n        // 记录所有文件信息\n        if (File.Exists(output))\n        {\n            OutputFiles.Add(new OutputFile()\n            {\n                Index = task.Id,\n                FilePath = output,\n                LangCode = streamSpec.Language,\n                Description = streamSpec.Name,\n                Mediainfos = mediaInfos,\n                MediaType = streamSpec.MediaType,\n            });\n        }\n\n        return true;\n    }\n\n    public async Task<bool> StartDownloadAsync()\n    {\n        ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算\n        ConcurrentDictionary<StreamSpec, bool?> Results = new();\n            \n        var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);\n        progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;\n\n        // 进度条的列定义\n        var progressColumns = new ProgressColumn[]\n        {\n            new TaskDescriptionColumn() { Alignment = Justify.Left },\n            new ProgressBarColumn(){ Width = 30 },\n            new MyPercentageColumn(),\n            new DownloadStatusColumn(SpeedContainerDic),\n            new DownloadSpeedColumn(SpeedContainerDic), // 速度计算\n            new RemainingTimeColumn(),\n            new SpinnerColumn(),\n        };\n        if (DownloaderConfig.MyOptions.NoAnsiColor)\n        {\n            progressColumns = progressColumns.SkipLast(1).ToArray();\n        }\n        progress.Columns(progressColumns);\n\n        if (DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, DecryptionEngine: not DecryptEngine.SHAKA_PACKAGER, Keys.Length: > 0 })\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.realTimeDecMessage}[/]\");\n\n        await progress.StartAsync(async ctx =>\n        {\n            // 创建任务\n            var dic = SelectedSteams.Select(item =>\n            {\n                var description = item.ToShortShortString();\n                var task = ctx.AddTask(description, autoStart: false);\n                SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算\n                // 限速设置\n                if (DownloaderConfig.MyOptions.MaxSpeed != null)\n                {\n                    SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value;\n                }\n                return (item, task);\n            }).ToDictionary(item => item.item, item => item.task);\n\n            if (!DownloaderConfig.MyOptions.ConcurrentDownload)\n            {\n                // 遍历，顺序下载\n                foreach (var kp in dic)\n                {\n                    var task = kp.Value;\n                    var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);\n                    Results[kp.Key] = result;\n                    // 失败不再下载后续\n                    if (!result) break;\n                }\n            }\n            else\n            {\n                // 并发下载\n                await Parallel.ForEachAsync(dic, async (kp, _) =>\n                {\n                    var task = kp.Value;\n                    var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);\n                    Results[kp.Key] = result;\n                });\n            }\n        });\n\n        var success = Results.Values.All(v => v == true);\n\n        // 删除临时文件夹\n        if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && success)\n        {\n            foreach (var item in StreamExtractor.RawFiles)\n            {\n                var file = Path.Combine(DownloaderConfig.DirPrefix, item.Key);\n                if (File.Exists(file)) File.Delete(file);\n            }\n            OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix);\n        }\n\n        // 混流\n        if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) \n        {\n            OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList();\n            // 是否跳过字幕\n            if (DownloaderConfig.MyOptions.MuxOptions!.SkipSubtitle)\n            {\n                OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList();\n            }\n            if (DownloaderConfig.MyOptions.MuxImports != null)\n            {\n                OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports);\n            }\n            OutputFiles.ForEach(f => Logger.WarnMarkUp($\"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]\"));\n            var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;\n            var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat);\n            var dirName = Path.GetFileName(DownloaderConfig.DirPrefix);\n            var outName = $\"{dirName}.MUX\";\n            var outPath = Path.Combine(saveDir, outName);\n            Logger.WarnMarkUp($\"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]\");\n            var result = false;\n            if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath);\n            else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo);\n            // 完成后删除各轨道文件\n            if (result)\n            {\n                if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles)\n                {\n                    Logger.WarnMarkUp(\"[grey]Cleaning files...[/]\");\n                    OutputFiles.ForEach(f => File.Delete(f.FilePath));\n                    var tmpDir = DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory;\n                    OtherUtil.SafeDeleteDir(tmpDir);\n                }\n            }\n            else\n            {\n                success = false;\n                Logger.ErrorMarkUp($\"Mux failed\");\n            }\n            // 判断是否要改名\n            var newPath = Path.ChangeExtension(outPath, ext);\n            if (result && !File.Exists(newPath))\n            {\n                Logger.WarnMarkUp($\"Rename to [grey]{Path.GetFileName(newPath).EscapeMarkup()}[/]\");\n                File.Move(outPath + ext, newPath);\n            }\n        }\n\n        return success;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs",
    "content": "﻿using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Column;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Config;\nusing N_m3u8DL_RE.Downloader;\nusing N_m3u8DL_RE.Entity;\nusing N_m3u8DL_RE.Parser;\nusing N_m3u8DL_RE.Parser.Mp4;\nusing N_m3u8DL_RE.Util;\nusing Spectre.Console;\nusing System.Collections.Concurrent;\nusing System.IO.Pipes;\nusing System.Text;\nusing System.Threading.Tasks.Dataflow;\nusing N_m3u8DL_RE.Enum;\n\nnamespace N_m3u8DL_RE.DownloadManager;\n\ninternal class SimpleLiveRecordManager2\n{\n    IDownloader Downloader;\n    DownloaderConfig DownloaderConfig;\n    StreamExtractor StreamExtractor;\n    List<StreamSpec> SelectedSteams;\n    ConcurrentDictionary<int, string> PipeSteamNamesDic = new();\n    List<OutputFile> OutputFiles = [];\n    DateTime? PublishDateTime;\n    bool STOP_FLAG = false;\n    int WAIT_SEC = 0; // 刷新间隔\n    ConcurrentDictionary<int, int> RecordedDurDic = new(); // 已录制时长\n    ConcurrentDictionary<int, int> RefreshedDurDic = new(); // 已刷新出的时长\n    ConcurrentDictionary<int, BufferBlock<List<MediaSegment>>> BlockDic = new(); // 各流的Block\n    ConcurrentDictionary<int, bool> SamePathDic = new(); // 各流是否allSamePath\n    ConcurrentDictionary<int, bool> RecordLimitReachedDic = new(); // 各流是否达到上限\n    ConcurrentDictionary<int, string> LastFileNameDic = new(); // 上次下载的文件名\n    ConcurrentDictionary<int, long> MaxIndexDic = new(); // 最大Index\n    ConcurrentDictionary<int, long> DateTimeDic = new(); // 上次下载的dateTime\n    CancellationTokenSource CancellationTokenSource = new(); // 取消Wait\n\n    private readonly Lock lockObj = new();\n    TimeSpan? audioStart = null;\n\n    public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)\n    {\n        this.DownloaderConfig = downloaderConfig;\n        Downloader = new SimpleDownloader(DownloaderConfig);\n        PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;\n        StreamExtractor = streamExtractor;\n        SelectedSteams = selectedSteams;\n    }\n\n    // 从文件读取KEY\n    private async Task SearchKeyAsync(string? currentKID)\n    {\n        var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID);\n        if (_key != null)\n        {\n            if (DownloaderConfig.MyOptions.Keys == null)\n                DownloaderConfig.MyOptions.Keys = [_key];\n            else\n                DownloaderConfig.MyOptions.Keys = [..DownloaderConfig.MyOptions.Keys, _key];\n        }\n    }\n\n    /// <summary>\n    /// 获取时间戳\n    /// </summary>\n    /// <param name=\"dateTime\"></param>\n    /// <returns></returns>\n    private long GetUnixTimestamp(DateTime dateTime)\n    {\n        return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds();\n    }\n\n    /// <summary>\n    /// 获取分段文件夹\n    /// </summary>\n    /// <param name=\"segment\"></param>\n    /// <param name=\"allHasDatetime\"></param>\n    /// <returns></returns>\n    private string GetSegmentName(MediaSegment segment, bool allHasDatetime, bool allSamePath)\n    {\n        if (!string.IsNullOrEmpty(segment.NameFromVar))\n        {\n            return segment.NameFromVar;\n        }\n\n        bool hls = StreamExtractor.ExtractorType == ExtractorType.HLS;\n\n        string name = OtherUtil.GetFileNameFromInput(segment.Url, false);\n        if (allSamePath)\n        {\n            name = OtherUtil.GetValidFileName(segment.Url.Split('?').Last(), \"_\");\n        }\n\n        if (hls && allHasDatetime)\n        {\n            name = GetUnixTimestamp(segment.DateTime!.Value).ToString();\n        }\n        else if (hls)\n        {\n            name = segment.Index.ToString();\n        }\n\n        return name;\n    }\n\n    private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)\n    {\n        if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))\n        {\n            DownloaderConfig.MyOptions.BinaryMerge = true;\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge2}[/]\");\n        }\n\n        if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))\n        {\n            DownloaderConfig.MyOptions.MuxAfterDone = false;\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge5}[/]\");\n        }\n\n        if (mediainfos.Where(m => m.Type == \"Audio\").All(m => m.BaseInfo!.Contains(\"aac\")))\n        {\n            useAACFilter = true;\n        }\n\n        if (mediainfos.All(m => m.Type == \"Audio\") && streamSpec.MediaType != MediaType.AUDIO)\n        {\n            streamSpec.MediaType = MediaType.AUDIO;\n        }\n        else if (mediainfos.All(m => m.Type == \"Subtitle\") && streamSpec.MediaType != MediaType.SUBTITLES)\n        {\n            streamSpec.MediaType = MediaType.SUBTITLES;\n\n            if (streamSpec.Extension is null or \"ts\")\n                streamSpec.Extension = \"vtt\";\n        }\n    }\n\n    private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer, BufferBlock<List<MediaSegment>> source)\n    {\n        var baseTimestamp = PublishDateTime == null ? 0L : (long)(PublishDateTime.Value.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds;\n        var decryptionBinaryPath = DownloaderConfig.MyOptions.DecryptionBinaryPath!;\n        var mp4InitFile = \"\";\n        var currentKID = \"\";\n        var readInfo = false; // 是否读取过\n        bool useAACFilter = false; // ffmpeg合并flag\n        bool initDownloaded = false; // 是否下载过init文件\n        ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();\n        List<Mediainfo> mediaInfos = [];\n        Stream? fileOutputStream = null;\n        WebVttSub currentVtt = new(); // 字幕流始终维护一个实例\n        bool firstSub = true;\n        task.StartTask();\n\n        var name = streamSpec.ToShortString();\n        var type = streamSpec.MediaType ?? Common.Enum.MediaType.VIDEO;\n        var dirName = $\"{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? \"\", \"-\")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}\";\n        var tmpDir = Path.Combine(DownloaderConfig.DirPrefix, dirName);\n        var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;\n\n        // Use SavePattern if provided, otherwise use SaveName or dirName\n        var saveName = dirName;\n        if (!string.IsNullOrWhiteSpace(DownloaderConfig.MyOptions.SavePattern))\n        {\n            saveName = OtherUtil.FormatSavePattern(DownloaderConfig.MyOptions.SavePattern, streamSpec, DownloaderConfig.MyOptions.SaveName, task.Id);\n        }\n        else if (DownloaderConfig.MyOptions.SaveName != null)\n        {\n            saveName = $\"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}\".TrimEnd('.');\n        }\n        var headers = DownloaderConfig.Headers;\n        var decryptEngine = DownloaderConfig.MyOptions.DecryptionEngine;\n\n        Logger.Debug($\"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}\");\n\n        // 创建文件夹\n        if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);\n        if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);\n\n        while (true && await source.OutputAvailableAsync())\n        {\n            // 接收新片段 且总是拿全部未处理的片段\n            // 有时每次只有很少的片段，但是之前的片段下载慢，导致后面还没下载的片段都失效了\n            // TryReceiveAll可以稍微缓解一下\n            source.TryReceiveAll(out IList<List<MediaSegment>>? segmentsList);\n            var segments = segmentsList!.SelectMany(s => s);\n            if (segments == null || !segments.Any()) continue;\n            var segmentsDuration = segments.Sum(s => s.Duration);\n            Logger.DebugMarkUp(string.Join(\",\", segments.Select(sss => GetSegmentName(sss, false, false))));\n\n            // 下载init\n            if (!initDownloaded && streamSpec.Playlist?.MediaInit != null) \n            {\n                task.MaxValue += 1;\n                // 对于fMP4，自动开启二进制合并\n                if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES)\n                {\n                    DownloaderConfig.MyOptions.BinaryMerge = true;\n                    Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge}[/]\");\n                }\n\n                var path = Path.Combine(tmpDir, \"_init.mp4.tmp\");\n                var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);\n                FileDic[streamSpec.Playlist.MediaInit] = result;\n                if (result is not { Success: true })\n                {\n                    throw new Exception(\"Download init file failed!\");\n                }\n                mp4InitFile = result.ActualFilePath;\n                task.Increment(1);\n\n                // 读取mp4信息\n                if (result is { Success: true })\n                {\n                    currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;\n                    // 从文件读取KEY\n                    await SearchKeyAsync(currentKID);\n                    // 实时解密\n                    if ((streamSpec.Playlist.MediaInit.IsEncrypted || !string.IsNullOrEmpty(currentKID)) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS)\n                    {\n                        var enc = result.ActualFilePath;\n                        var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                        var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);\n                        if (dResult)\n                        {\n                            FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec;\n                        }\n                    }\n                    // ffmpeg读取信息\n                    if (!readInfo)\n                    {\n                        Logger.WarnMarkUp(ResString.readingInfo);\n                        mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result.ActualFilePath);\n                        mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));\n                        lock (lockObj)\n                        {\n                            if (audioStart == null) audioStart = mediaInfos.FirstOrDefault(x => x.Type == \"Audio\")?.StartTime;\n                        }\n                        ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter);\n                        readInfo = true;\n                    }\n                    initDownloaded = true;\n                }\n            }\n\n            var allHasDatetime = segments.All(s => s.DateTime != null);\n            if (!SamePathDic.ContainsKey(task.Id))\n            {\n                var allName = segments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false));\n                var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1;\n                SamePathDic[task.Id] = allSamePath;\n            }\n\n            // 下载第一个分片\n            if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS)\n            {\n                var seg = segments.First();\n                segments = segments.Skip(1);\n                // 获取文件名\n                var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]);\n                var index = seg.Index;\n                var path = Path.Combine(tmpDir, filename + $\".{streamSpec.Extension ?? \"clip\"}.tmp\");\n                var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);\n                FileDic[seg] = result;\n                if (result is not { Success: true })\n                {\n                    throw new Exception(\"Download first segment failed!\");\n                }\n                task.Increment(1);\n                if (result is { Success: true })\n                {\n                    // 修复MSS init\n                    if (StreamExtractor.ExtractorType == ExtractorType.MSS)\n                    {\n                        var processor = new MSSMoovProcessor(streamSpec);\n                        var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath));\n                        await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header);\n                        if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))\n                        {\n                            // 需要重新解密init\n                            var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath;\n                            var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                            var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);\n                            if (dResult)\n                            {\n                                FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec;\n                            }\n                        }\n                    }\n                    // 读取init信息\n                    if (string.IsNullOrEmpty(currentKID))\n                    {\n                        currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;\n                    }\n                    // 从文件读取KEY\n                    await SearchKeyAsync(currentKID);\n                    // 实时解密\n                    if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))\n                    {\n                        var enc = result.ActualFilePath;\n                        var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                        var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile);\n                        if (dResult)\n                        {\n                            File.Delete(enc);\n                            result.ActualFilePath = dec;\n                        }\n                    }\n                    if (!readInfo)\n                    {\n                        // ffmpeg读取信息\n                        Logger.WarnMarkUp(ResString.readingInfo);\n                        mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath);\n                        mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));\n                        lock (lockObj)\n                        {\n                            if (audioStart == null) audioStart = mediaInfos.FirstOrDefault(x => x.Type == \"Audio\")?.StartTime;\n                        }\n                        ChangeSpecInfo(streamSpec, mediaInfos, ref useAACFilter);\n                        readInfo = true;\n                    }\n                }\n            }\n\n            // 开始下载\n            var options = new ParallelOptions()\n            {\n                MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount\n            };\n            await Parallel.ForEachAsync(segments, options, async (seg, _) =>\n            {\n                // 获取文件名\n                var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]);\n                var index = seg.Index;\n                var path = Path.Combine(tmpDir, filename + $\".{streamSpec.Extension ?? \"clip\"}.tmp\");\n                var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);\n                FileDic[seg] = result;\n                if (result is { Success: true })\n                    task.Increment(1);\n                // 实时解密\n                if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result is { Success: true } && !string.IsNullOrEmpty(currentKID))\n                {\n                    var enc = result.ActualFilePath;\n                    var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + \"_dec\" + Path.GetExtension(enc));\n                    var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile);\n                    if (dResult)\n                    {\n                        File.Delete(enc);\n                        result.ActualFilePath = dec;\n                    }\n                }\n            });\n\n            // 自动修复VTT raw字幕\n            if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains(\"vtt\"))\n            {\n                // 排序字幕并修正时间戳\n                var keys = FileDic.Keys.OrderBy(k => k.Index).ToList();\n                foreach (var seg in keys)\n                {\n                    var vttContent = await File.ReadAllTextAsync(FileDic[seg]!.ActualFilePath);\n                    var waitCount = 0;\n                    while (DownloaderConfig.MyOptions.LiveFixVttByAudio && audioStart == null && waitCount++ < 5)\n                    {\n                        await Task.Delay(1000);\n                    }\n                    var subOffset = audioStart != null ? (long)audioStart.Value.TotalMilliseconds : 0L;\n                    var vtt = WebVttSub.Parse(vttContent, subOffset);\n                    // 手动计算MPEGTS\n                    if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                    {\n                        vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration);\n                    }\n                    if (firstSub) { currentVtt = vtt; firstSub = false; }\n                    else currentVtt.AddCuesFromOne(vtt);\n                }\n            }\n\n            // 自动修复VTT mp4字幕\n            if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES\n                                                           && streamSpec.Codecs != \"stpp\" && streamSpec.Extension != null && streamSpec.Extension.Contains(\"m4s\"))\n            {\n                var initFile = FileDic.Values.FirstOrDefault(v => Path.GetFileName(v!.ActualFilePath).StartsWith(\"_init\"));\n                var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);\n                var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);\n                if (sawVtt)\n                {\n                    var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(\".m4s\")).ToArray();\n                    if (firstSub)\n                    {\n                        currentVtt = MP4VttUtil.ExtractSub(mp4s, timescale);\n                        firstSub = false;\n                    }\n                    else\n                    {\n                        var vtt = MP4VttUtil.ExtractSub(mp4s, timescale);\n                        currentVtt.AddCuesFromOne(vtt);\n                    }\n                }\n            }\n\n            // 自动修复TTML raw字幕\n            if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains(\"ttml\"))\n            {\n                var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(\".m4s\")).Select(s => s.Key).ToList();\n                if (firstSub)\n                {\n                    if (baseTimestamp != 0)\n                    {\n                        var total = segmentsDuration;\n                        baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds;\n                    }\n                    var first = true;\n                    foreach (var seg in keys)\n                    {\n                        var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);\n                        // 手动计算MPEGTS\n                        if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                        {\n                            vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration);\n                        }\n                        if (first) { currentVtt = vtt; first = false; }\n                        else currentVtt.AddCuesFromOne(vtt);\n                    }\n                    firstSub = false;\n                }\n                else\n                {\n                    foreach (var seg in keys)\n                    {\n                        var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);\n                        // 手动计算MPEGTS\n                        if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                        {\n                            vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));\n                        }\n                        currentVtt.AddCuesFromOne(vtt);\n                    }\n                }\n            }\n\n            // 自动修复TTML mp4字幕\n            if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains(\"m4s\")\n                && streamSpec.Codecs != null && streamSpec.Codecs.Contains(\"stpp\"))\n            {\n                // sawTtml暂时不判断\n                // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith(\"_init\")).FirstOrDefault();\n                // var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);\n                // var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);\n                var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(\".m4s\")).Select(s => s.Key);\n                if (firstSub)\n                {\n                    if (baseTimestamp != 0)\n                    {\n                        var total = segmentsDuration;\n                        baseTimestamp -= (long)TimeSpan.FromSeconds(total).TotalMilliseconds;\n                    }\n                    var first = true;\n                    foreach (var seg in keys)\n                    {\n                        var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);\n                        // 手动计算MPEGTS\n                        if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                        {\n                            vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration);\n                        }\n                        if (first) { currentVtt = vtt; first = false; }\n                        else currentVtt.AddCuesFromOne(vtt);\n                    }\n                    firstSub = false;\n                }\n                else\n                {\n                    foreach (var seg in keys)\n                    {\n                        var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);\n                        // 手动计算MPEGTS\n                        if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)\n                        {\n                            vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));\n                        }\n                        currentVtt.AddCuesFromOne(vtt);\n                    }\n                }\n            }\n\n            RecordedDurDic[task.Id] += (int)segmentsDuration;\n\n            /*// 写出m3u8\n            if (DownloaderConfig.MyOptions.LiveWriteHLS)\n            {\n                var _saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;\n                var _saveName = DownloaderConfig.MyOptions.SaveName ?? DateTime.Now.ToString(\"yyyyMMddHHmmss\");\n                await StreamingUtil.WriteStreamListAsync(FileDic, task.Id, 0, _saveName, _saveDir);\n            }*/\n\n            // 合并逻辑\n            if (DownloaderConfig.MyOptions.LiveRealTimeMerge)\n            {\n                // 合并\n                var outputExt = \".\" + streamSpec.Extension;\n                if (streamSpec.Extension == null) outputExt = \".ts\";\n                else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: \"m4s\" }) outputExt = \".m4a\";\n                else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension == \"m4s\") outputExt = \".mp4\";\n                else if (streamSpec.MediaType == MediaType.SUBTITLES)\n                {\n                    outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? \".srt\" : \".vtt\";\n                }\n\n                var output = Path.Combine(saveDir, saveName + outputExt);\n\n                // 移除无效片段\n                var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key);\n                foreach (var badKey in badKeys)\n                {\n                    FileDic!.Remove(badKey, out _);\n                }\n\n                // 设置输出流\n                if (fileOutputStream == null)\n                {\n                    // 检测目标文件是否存在，使用智能重命名\n                    var finalOutput = OtherUtil.HandleFileCollision(output, streamSpec);\n                    if (finalOutput != output)\n                    {\n                        Logger.WarnMarkUp($\"{Path.GetFileName(output)} => {Path.GetFileName(finalOutput)}\");\n                        output = finalOutput;\n                    }\n\n                    if (!DownloaderConfig.MyOptions.LivePipeMux || streamSpec.MediaType == MediaType.SUBTITLES)\n                    {\n                        fileOutputStream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);\n                    }\n                    else \n                    {\n                        // 创建管道\n                        output = Path.ChangeExtension(output, \".ts\");\n                        var pipeName = $\"RE_pipe_{Guid.NewGuid()}\";\n                        fileOutputStream = PipeUtil.CreatePipe(pipeName);\n                        Logger.InfoMarkUp($\"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]\");\n                        PipeSteamNamesDic[task.Id] = pipeName;\n                        if (PipeSteamNamesDic.Count == SelectedSteams.Count(x => x.MediaType != MediaType.SUBTITLES)) \n                        {\n                            var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray();\n                            Logger.WarnMarkUp($\"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]\");\n                            var t = PipeUtil.StartPipeMuxAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, names, output);\n                        }\n\n                        // Windows only\n                        if (OperatingSystem.IsWindows())\n                            await (fileOutputStream as NamedPipeServerStream)!.WaitForConnectionAsync();\n                    }\n                }\n\n                if (streamSpec.MediaType != MediaType.SUBTITLES)\n                {\n                    var initResult = streamSpec.Playlist!.MediaInit != null ? FileDic[streamSpec.Playlist!.MediaInit!]! : null;\n                    var files = FileDic.Where(f => f.Key != streamSpec.Playlist!.MediaInit).OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray();\n                    if (initResult != null && mp4InitFile != \"\")\n                    {\n                        // shaka/ffmpeg实时解密不需要init文件用于合并，mp4decrpyt需要\n                        if (string.IsNullOrEmpty(currentKID) || decryptEngine == DecryptEngine.MP4DECRYPT)\n                        {\n                            files = [initResult.ActualFilePath, ..files];\n                        }\n                    }\n                    foreach (var inputFilePath in files)\n                    {\n                        using (var inputStream = File.OpenRead(inputFilePath))\n                        {\n                            inputStream.CopyTo(fileOutputStream);\n                        }\n                    }\n                    if (!DownloaderConfig.MyOptions.LiveKeepSegments)\n                    {\n                        foreach (var inputFilePath in files.Where(x => !Path.GetFileName(x).StartsWith(\"_init\")))\n                        {\n                            File.Delete(inputFilePath);\n                        }\n                    }\n                    FileDic.Clear();\n                    if (initResult != null)\n                    {\n                        FileDic[streamSpec.Playlist!.MediaInit!] = initResult;\n                    }\n                }\n                else\n                {\n                    var initResult = streamSpec.Playlist!.MediaInit != null ? FileDic[streamSpec.Playlist!.MediaInit!]! : null;\n                    var files = FileDic.OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray();\n                    foreach (var inputFilePath in files)\n                    {\n                        if (!DownloaderConfig.MyOptions.LiveKeepSegments && !Path.GetFileName(inputFilePath).StartsWith(\"_init\"))\n                        {\n                            File.Delete(inputFilePath);\n                        }\n                    }\n\n                    // 处理图形字幕\n                    await SubtitleUtil.TryWriteImagePngsAsync(currentVtt, tmpDir);\n\n                    var subText = currentVtt.ToVtt();\n                    if (outputExt == \".srt\")\n                    {\n                        subText = currentVtt.ToSrt();\n                    }\n                    var subBytes = Encoding.UTF8.GetBytes(subText);\n                    fileOutputStream.Position = 0;\n                    fileOutputStream.Write(subBytes);\n                    FileDic.Clear();\n                    if (initResult != null)\n                    {\n                        FileDic[streamSpec.Playlist!.MediaInit!] = initResult;\n                    }\n                }\n\n                // 刷新buffer\n                if (fileOutputStream != null)\n                {\n                    fileOutputStream.Flush();\n                }\n            }\n\n            if (STOP_FLAG && source.Count == 0) \n                break;\n        }\n\n        if (fileOutputStream == null) return true;\n        \n        if (!DownloaderConfig.MyOptions.LivePipeMux)\n        {\n            // 记录所有文件信息\n            OutputFiles.Add(new OutputFile()\n            {\n                Index = task.Id,\n                FilePath = (fileOutputStream as FileStream)!.Name,\n                LangCode = streamSpec.Language,\n                Description = streamSpec.Name,\n                Mediainfos = mediaInfos,\n                MediaType = streamSpec.MediaType,\n            });\n        }\n        fileOutputStream.Close();\n        fileOutputStream.Dispose();\n\n        return true;\n    }\n\n    private async Task PlayListProduceAsync(Dictionary<StreamSpec, ProgressTask> dic)\n    {\n        while (!STOP_FLAG)\n        {\n            if (WAIT_SEC == 0) continue;\n            \n            // 1. MPD 所有URL相同 单次请求即可获得所有轨道的信息\n            // 2. M3U8 所有URL不同 才需要多次请求\n            await Parallel.ForEachAsync(dic, async (dic, _) =>\n            {\n                var streamSpec = dic.Key;\n                var task = dic.Value;\n\n                // 达到上限时 不需要刷新了\n                if (RecordLimitReachedDic[task.Id])\n                    return;\n\n                var allHasDatetime = streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null);\n                if (!SamePathDic.ContainsKey(task.Id))\n                {\n                    var allName = streamSpec.Playlist!.MediaParts[0].MediaSegments.Select(s => OtherUtil.GetFileNameFromInput(s.Url, false));\n                    var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1;\n                    SamePathDic[task.Id] = allSamePath;\n                }\n                // 过滤不需要下载的片段\n                FilterMediaSegments(streamSpec, task, allHasDatetime, SamePathDic[task.Id]);\n                var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments;\n                if (newList.Count > 0)\n                {\n                    task.MaxValue += newList.Count;\n                    // 推送给消费者\n                    await BlockDic[task.Id].SendAsync(newList);\n                    // 更新最新链接\n                    LastFileNameDic[task.Id] = GetSegmentName(newList.Last(), allHasDatetime, SamePathDic[task.Id]);\n                    // 尝试更新时间戳\n                    var dt = newList.Last().DateTime;\n                    DateTimeDic[task.Id] = dt != null ? GetUnixTimestamp(dt.Value) : 0L;\n                    // 累加已获取到的时长\n                    RefreshedDurDic[task.Id] += (int)newList.Sum(s => s.Duration);\n                }\n\n                if (!STOP_FLAG && RefreshedDurDic[task.Id] >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds)\n                {\n                    RecordLimitReachedDic[task.Id] = true;\n                }\n\n                // 检测时长限制\n                if (!STOP_FLAG && RecordLimitReachedDic.Values.All(x => x))\n                {\n                    Logger.WarnMarkUp($\"[darkorange3_1]{ResString.liveLimitReached}[/]\");\n                    STOP_FLAG = true;\n                    CancellationTokenSource.Cancel();\n                }\n            });\n\n            try\n            {\n                // Logger.WarnMarkUp($\"wait {waitSec}s\");\n                if (!STOP_FLAG) await Task.Delay(WAIT_SEC * 1000, CancellationTokenSource.Token);\n                // 刷新列表\n                if (!STOP_FLAG) await StreamExtractor.RefreshPlayListAsync(dic.Keys.ToList());\n            }\n            catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)\n            {\n                // 不需要做事\n            }\n            catch (Exception e)\n            {\n                Logger.ErrorMarkUp(e);\n                STOP_FLAG = true;\n                // 停止所有Block\n                foreach (var target in BlockDic.Values)\n                {\n                    target.Complete();\n                }\n            }\n        }\n    }\n\n    private void FilterMediaSegments(StreamSpec streamSpec, ProgressTask task, bool allHasDatetime, bool allSamePath)\n    {\n        if (string.IsNullOrEmpty(LastFileNameDic[task.Id]) && DateTimeDic[task.Id] == 0) return;\n\n        var index = -1;\n        var dateTime = DateTimeDic[task.Id];\n        var lastName = LastFileNameDic[task.Id];\n\n        // 优先使用dateTime判断\n        if (dateTime != 0 && streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null)) \n        {\n            index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetUnixTimestamp(s.DateTime!.Value) == dateTime);\n        }\n        else\n        {\n            index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetSegmentName(s, allHasDatetime, allSamePath) == lastName);\n        }\n\n        if (index > -1)\n        {\n            // 修正Index\n            var list = streamSpec.Playlist!.MediaParts[0].MediaSegments.Skip(index + 1).ToList();\n            if (list.Count > 0)\n            {\n                var newMin = list.Min(s => s.Index);\n                var oldMax = MaxIndexDic[task.Id];\n                if (newMin < oldMax)\n                {\n                    var offset = oldMax - newMin + 1;\n                    foreach (var item in list)\n                    {\n                        item.Index += offset;\n                    }\n                }\n                MaxIndexDic[task.Id] = list.Max(s => s.Index);\n            }\n            streamSpec.Playlist!.MediaParts[0].MediaSegments = list;\n        }\n    }\n\n    public async Task<bool> StartRecordAsync()\n    {\n        var takeLastCount = DownloaderConfig.MyOptions.LiveTakeCount;\n        ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算\n        ConcurrentDictionary<StreamSpec, bool?> Results = new();\n        // 同步流\n        FilterUtil.SyncStreams(SelectedSteams, takeLastCount);\n        // 设置等待时间\n        if (WAIT_SEC == 0)\n        {\n            WAIT_SEC = (int)(SelectedSteams.Min(s => s.Playlist!.MediaParts[0].MediaSegments.Sum(s => s.Duration)) / 2);\n            WAIT_SEC -= 2; // 再提前两秒吧 留出冗余\n            if (DownloaderConfig.MyOptions.LiveWaitTime != null)\n                WAIT_SEC = DownloaderConfig.MyOptions.LiveWaitTime.Value;\n            if (WAIT_SEC <= 0) WAIT_SEC = 1;\n            Logger.WarnMarkUp($\"set refresh interval to {WAIT_SEC} seconds\");\n        }\n        // 如果没有选中音频 取消通过音频修复vtt时间轴\n        if (SelectedSteams.All(x => x.MediaType != MediaType.AUDIO))\n        {\n            DownloaderConfig.MyOptions.LiveFixVttByAudio = false;\n        }\n\n        /*// 写出master\n        if (DownloaderConfig.MyOptions.LiveWriteHLS)\n        {\n            var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;\n            var saveName = DownloaderConfig.MyOptions.SaveName ?? DateTime.Now.ToString(\"yyyyMMddHHmmss\");\n            await StreamingUtil.WriteMasterListAsync(SelectedSteams, saveName, saveDir);\n        }*/\n\n        var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);\n        progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;\n            \n        // 进度条的列定义\n        var progressColumns = new ProgressColumn[]\n        {\n            new TaskDescriptionColumn() { Alignment = Justify.Left },\n            new RecordingDurationColumn(RecordedDurDic, RefreshedDurDic), // 时长显示\n            new RecordingStatusColumn(),\n            new PercentageColumn(),\n            new DownloadSpeedColumn(SpeedContainerDic), // 速度计算\n            new SpinnerColumn(),\n        };\n        if (DownloaderConfig.MyOptions.NoAnsiColor)\n        {\n            progressColumns = progressColumns.SkipLast(1).ToArray();\n        }\n        progress.Columns(progressColumns);\n\n        await progress.StartAsync(async ctx =>\n        {\n            // 创建任务\n            var dic = SelectedSteams.Select(item =>\n            {\n                var task = ctx.AddTask(item.ToShortShortString(), autoStart: false, maxValue: 0);\n                SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算\n                // 限速设置\n                if (DownloaderConfig.MyOptions.MaxSpeed != null)\n                {\n                    SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value;\n                }\n                LastFileNameDic[task.Id] = \"\";\n                RecordLimitReachedDic[task.Id] = false;\n                DateTimeDic[task.Id] = 0L;\n                RecordedDurDic[task.Id] = 0;\n                RefreshedDurDic[task.Id] = 0;\n                MaxIndexDic[task.Id] = item.Playlist?.MediaParts[0].MediaSegments.LastOrDefault()?.Index ?? 0L; // 最大Index\n                BlockDic[task.Id] = new BufferBlock<List<MediaSegment>>();\n                return (item, task);\n            }).ToDictionary(item => item.item, item => item.task);\n\n            DownloaderConfig.MyOptions.ConcurrentDownload = true;\n            DownloaderConfig.MyOptions.MP4RealTimeDecryption = true;\n            DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;\n            if (DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, DecryptionEngine: not DecryptEngine.SHAKA_PACKAGER, Keys.Length: > 0 })\n                Logger.WarnMarkUp($\"[darkorange3_1]{ResString.realTimeDecMessage}[/]\");\n            var limit = DownloaderConfig.MyOptions.LiveRecordLimit;\n            if (limit != TimeSpan.MaxValue)\n                Logger.WarnMarkUp($\"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]\");\n            // 录制直播时，用户选了几个流就并发录几个\n            var options = new ParallelOptions()\n            {\n                MaxDegreeOfParallelism = SelectedSteams.Count\n            };\n            // 开始刷新\n            var producerTask = PlayListProduceAsync(dic);\n            await Task.Delay(200);\n            // 并发下载\n            await Parallel.ForEachAsync(dic, options, async (kp, _) =>\n            {\n                var task = kp.Value;\n                var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id], BlockDic[task.Id]);\n                Results[kp.Key] = await consumerTask;\n            });\n        });\n\n        var success = Results.Values.All(v => v == true);\n\n        // 删除临时文件夹\n        if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && success)\n        {\n            foreach (var item in StreamExtractor.RawFiles)\n            {\n                var file = Path.Combine(DownloaderConfig.DirPrefix, item.Key);\n                if (File.Exists(file)) File.Delete(file);\n            }\n            OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix);\n        }\n\n        // 混流\n        if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0)\n        {\n            OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList();\n            // 是否跳过字幕\n            if (DownloaderConfig.MyOptions.MuxOptions!.SkipSubtitle)\n            {\n                OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList();\n            }\n            if (DownloaderConfig.MyOptions.MuxImports != null)\n            {\n                OutputFiles.AddRange(DownloaderConfig.MyOptions.MuxImports);\n            }\n            OutputFiles.ForEach(f => Logger.WarnMarkUp($\"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]\"));\n            var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;\n            var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat);\n            var dirName = Path.GetFileName(DownloaderConfig.DirPrefix);\n            var outName = $\"{dirName}.MUX\";\n            var outPath = Path.Combine(saveDir, outName);\n            Logger.WarnMarkUp($\"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]\");\n            var result = false;\n            if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath);\n            else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo);\n            // 完成后删除各轨道文件\n            if (result)\n            {\n                if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles)\n                {\n                    Logger.WarnMarkUp(\"[grey]Cleaning files...[/]\");\n                    OutputFiles.ForEach(f => File.Delete(f.FilePath));\n                    var tmpDir = DownloaderConfig.MyOptions.TmpDir ?? Environment.CurrentDirectory;\n                    OtherUtil.SafeDeleteDir(tmpDir);\n                }\n            }\n            else\n            {\n                success = false;\n                Logger.ErrorMarkUp($\"Mux failed\");\n            }\n            // 判断是否要改名\n            var newPath = Path.ChangeExtension(outPath, ext);\n            if (result && !File.Exists(newPath))\n            {\n                Logger.WarnMarkUp($\"Rename to [grey]{Path.GetFileName(newPath).EscapeMarkup()}[/]\");\n                File.Move(outPath + ext, newPath);\n            }\n        }\n\n        return success;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Downloader/IDownloader.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Entity;\n\nnamespace N_m3u8DL_RE.Downloader;\n\ninternal interface IDownloader\n{\n    Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null);\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Downloader/SimpleDownloader.cs",
    "content": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Config;\nusing N_m3u8DL_RE.Crypto;\nusing N_m3u8DL_RE.Entity;\nusing N_m3u8DL_RE.Util;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Downloader;\n\n/// <summary>\n/// 简单下载器\n/// </summary>\ninternal class SimpleDownloader : IDownloader\n{\n    DownloaderConfig DownloaderConfig;\n\n    public SimpleDownloader(DownloaderConfig config)\n    {\n        DownloaderConfig = config;\n    }\n\n    public async Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null)\n    {\n        var url = segment.Url;\n        var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount);\n        if (dResult is { Success: true } && dResult.ActualFilePath != des)\n        {\n            switch (segment.EncryptInfo.Method)\n            {\n                case EncryptMethod.AES_128:\n                {\n                    var key = segment.EncryptInfo.Key;\n                    var iv = segment.EncryptInfo.IV;\n                    AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!);\n                    break;\n                }\n                case EncryptMethod.AES_128_ECB:\n                {\n                    var key = segment.EncryptInfo.Key;\n                    var iv = segment.EncryptInfo.IV;\n                    AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB);\n                    break;\n                }\n                case EncryptMethod.CHACHA20:\n                {\n                    var key = segment.EncryptInfo.Key;\n                    var nonce = segment.EncryptInfo.IV;\n\n                    var fileBytes = File.ReadAllBytes(dResult.ActualFilePath);\n                    var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!);\n                    await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted);\n                    break;\n                }\n                case EncryptMethod.SAMPLE_AES_CTR:\n                    // throw new NotSupportedException(\"SAMPLE-AES-CTR\");\n                    break;\n            }\n\n            // Image头处理\n            if (dResult.ImageHeader)\n            {\n                await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);\n            }\n            // Gzip解压\n            if (dResult.GzipHeader)\n            {\n                await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);\n            }\n\n            // 处理完成后改名\n            File.Move(dResult.ActualFilePath, des);\n            dResult.ActualFilePath = des;\n        }\n        return dResult;\n    }\n\n    private async Task<(string des, DownloadResult? dResult)> DownClipAsync(string url, string path, SpeedContainer speedContainer, long? fromPosition, long? toPosition, Dictionary<string, string>? headers = null, int retryCount = 3)\n    {\n        CancellationTokenSource? cancellationTokenSource = null;\n        retry:\n        try\n        {\n            cancellationTokenSource = new();\n            var des = Path.ChangeExtension(path, null);\n\n            // 已下载跳过\n            if (File.Exists(des))\n            {\n                speedContainer.Add(new FileInfo(des).Length);\n                return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des });\n            }\n\n            // 已解密跳过\n            var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + \"_dec\" + Path.GetExtension(des));\n            if (File.Exists(dec))\n            {\n                speedContainer.Add(new FileInfo(dec).Length);\n                return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec });\n            }\n\n            // 另起线程进行监控\n            var cts = cancellationTokenSource;\n            using var watcher = Task.Factory.StartNew(async () =>\n            {\n                while (true)\n                {\n                    if (cts.IsCancellationRequested) break;\n                    if (speedContainer.ShouldStop)\n                    {\n                        cts.Cancel();\n                        Logger.DebugMarkUp(\"Cancel...\");\n                        break;\n                    }\n                    await Task.Delay(500);\n                }\n            });\n\n            // 调用下载\n            var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition);\n            return (des, result);\n\n            throw new Exception(\"please retry\");\n        }\n        catch (Exception ex)\n        {\n            Logger.DebugMarkUp($\"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]\");\n            Logger.Debug(url + \" \" + ex);\n            Logger.Extra($\"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception  => {ex.Message}{Environment.NewLine}Url        => {url}\");\n            if (retryCount-- > 0)\n            {\n                await Task.Delay(1000);\n                goto retry;\n            }\n            else\n            {\n                Logger.Extra($\"The retry attempts have been exhausted and the download of this segment has failed.{Environment.NewLine}Exception  => {ex.Message}{Environment.NewLine}Url        => {url}\");\n                Logger.WarnMarkUp($\"[grey]{ex.Message.EscapeMarkup()}[/]\");\n            }\n            // throw new Exception(\"download failed\", ex);\n            return default;\n        }\n        finally\n        {\n            if (cancellationTokenSource != null)\n            {\n                // 调用后销毁\n                cancellationTokenSource.Dispose();\n                cancellationTokenSource = null;\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/CustomRange.cs",
    "content": "﻿namespace N_m3u8DL_RE.Entity;\n\npublic class CustomRange\n{\n    public required string InputStr { get; set; }\n    public double? StartSec { get; set; }\n    public double? EndSec { get; set; }\n\n    public long? StartSegIndex { get; set; }\n    public long? EndSegIndex { get; set;}\n\n    public override string? ToString()\n    {\n        return $\"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}\";\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/DownloadResult.cs",
    "content": "﻿namespace N_m3u8DL_RE.Entity;\n\ninternal class DownloadResult\n{\n    public bool Success => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength != null);\n    public long? RespContentLength { get; set; }\n    public long? ActualContentLength { get; set; }\n    public bool ImageHeader { get; set; } = false; // 图片伪装\n    public bool GzipHeader { get; set; } = false; // GZip压缩\n    public required string ActualFilePath { get; set; }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/Mediainfo.cs",
    "content": "﻿using Spectre.Console;\n\nnamespace N_m3u8DL_RE.Entity;\n\ninternal class Mediainfo\n{\n    public string? Id { get; set; }\n    public string? Text { get; set; }\n    public string? BaseInfo { get; set; }\n    public string? Bitrate { get; set; }\n    public string? Resolution { get; set; }\n    public string? Fps { get; set; }\n    public string? Type { get; set; }\n    public TimeSpan? StartTime { get; set; }\n    public bool DolbyVison { get; set; }\n    public bool HDR { get; set; }\n\n    public override string? ToString()\n    {\n        return $\"{(string.IsNullOrEmpty(Id) ? \"NaN\" : Id)}: \" + string.Join(\", \", new List<string?> { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i)));\n    }\n\n    public string ToStringMarkUp()\n    {\n        return \"[steelblue]\" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? \" [darkorange3_1][[HDR]][/]\" : \"\") + (DolbyVison ? \" [darkorange3_1][[DOVI]][/]\" : \"\") + \"[/]\";\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/MuxOptions.cs",
    "content": "﻿using N_m3u8DL_RE.Enum;\n\nnamespace N_m3u8DL_RE.Entity;\n\ninternal class MuxOptions\n{\n    public bool UseMkvmerge { get; set; } = false;\n    public MuxFormat MuxFormat { get; set; } = MuxFormat.MP4;\n    public bool KeepFiles { get; set; } = false;\n    public bool SkipSubtitle { get; set; } = false;\n    public string? BinPath { get; set; }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/OutputFile.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Entity;\n\ninternal class OutputFile\n{\n    public MediaType? MediaType { get; set; }\n    public required int Index { get; set; }\n    public required string FilePath { get; set; }\n    public string? LangCode { get; set; }\n    public string? Description { get; set; }\n    public List<Mediainfo> Mediainfos { get; set; } = [];\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/SpeedContainer.cs",
    "content": "﻿namespace N_m3u8DL_RE.Entity;\n\ninternal class SpeedContainer\n{\n    public bool SingleSegment { get; set; } = false;\n    public long NowSpeed { get; set; } = 0L; // 当前每秒速度\n    public long SpeedLimit { get; set; } = long.MaxValue; // 限速设置\n    public long? ResponseLength { get; set; }\n    public long RDownloaded => _Rdownloaded;\n    private int _zeroSpeedCount = 0;\n    public int LowSpeedCount => _zeroSpeedCount;\n    public bool ShouldStop => LowSpeedCount >= 20;\n\n    ///////////////////////////////////////////////////\n\n    private long _downloaded = 0;\n    private long _Rdownloaded = 0;\n    public long Downloaded => _downloaded;\n\n    public int AddLowSpeedCount()\n    {\n        return Interlocked.Add(ref _zeroSpeedCount, 1);\n    }\n\n    public int ResetLowSpeedCount()\n    {\n        return Interlocked.Exchange(ref _zeroSpeedCount, 0);\n    }\n\n    public long Add(long size)\n    {\n        Interlocked.Add(ref _Rdownloaded, size);\n        return Interlocked.Add(ref _downloaded, size);\n    }\n\n    public void Reset()\n    {\n        Interlocked.Exchange(ref _downloaded, 0);\n    }\n\n    public void ResetVars()\n    {\n        Reset();\n        ResetLowSpeedCount();\n        SingleSegment = false;\n        ResponseLength = null;\n        _Rdownloaded = 0L;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Entity/StreamFilter.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Entity;\n\npublic class StreamFilter\n{\n    public Regex? GroupIdReg { get; set; }\n    public Regex? LanguageReg { get; set; }\n    public Regex? NameReg { get; set; }\n    public Regex? CodecsReg { get; set; }\n    public Regex? ResolutionReg { get; set; }\n    public Regex? FrameRateReg { get; set; }\n    public Regex? ChannelsReg { get; set; }\n    public Regex? VideoRangeReg { get; set; }\n    public Regex? UrlReg { get; set; }\n    public long? SegmentsMinCount { get; set; }\n    public long? SegmentsMaxCount { get; set; }\n    public double? PlaylistMinDur {  get; set; }\n    public double? PlaylistMaxDur {  get; set; }\n    public int? BandwidthMin { get; set; }\n    public int? BandwidthMax { get; set; }\n    public RoleType? Role { get; set; }\n\n    public string For { get; set; } = \"best\";\n\n    public override string? ToString()\n    {\n        var sb = new StringBuilder();\n\n        if (GroupIdReg != null) sb.Append($\"GroupIdReg: {GroupIdReg} \");\n        if (LanguageReg != null) sb.Append($\"LanguageReg: {LanguageReg} \");\n        if (NameReg != null) sb.Append($\"NameReg: {NameReg} \");\n        if (CodecsReg != null) sb.Append($\"CodecsReg: {CodecsReg} \");\n        if (ResolutionReg != null) sb.Append($\"ResolutionReg: {ResolutionReg} \");\n        if (FrameRateReg != null) sb.Append($\"FrameRateReg: {FrameRateReg} \");\n        if (ChannelsReg != null) sb.Append($\"ChannelsReg: {ChannelsReg} \");\n        if (VideoRangeReg != null) sb.Append($\"VideoRangeReg: {VideoRangeReg} \");\n        if (UrlReg != null) sb.Append($\"UrlReg: {UrlReg} \");\n        if (SegmentsMinCount != null) sb.Append($\"SegmentsMinCount: {SegmentsMinCount} \");\n        if (SegmentsMaxCount != null) sb.Append($\"SegmentsMaxCount: {SegmentsMaxCount} \");\n        if (PlaylistMinDur != null) sb.Append($\"PlaylistMinDur: {PlaylistMinDur} \");\n        if (PlaylistMaxDur != null) sb.Append($\"PlaylistMaxDur: {PlaylistMaxDur} \");\n        if (BandwidthMin != null) sb.Append($\"{nameof(BandwidthMin)}: {BandwidthMin} \");\n        if (BandwidthMax != null) sb.Append($\"{nameof(BandwidthMax)}: {BandwidthMax} \");\n        if (Role.HasValue) sb.Append($\"Role: {Role} \");\n\n        return sb + $\"For: {For}\";\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Enum/DecryptEngine.cs",
    "content": "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",
    "content": "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",
    "content": "﻿namespace N_m3u8DL_RE.Enum;\n\ninternal enum SubtitleFormat\n{\n    VTT,\n    SRT\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/N_m3u8DL-RE.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>N_m3u8DL_RE</RootNamespace>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <!-- \n      When enabling LangVersion preview, that enables first class span's which then prefers MemoryExtensions.Contains over Enumerable.Contains\n      See details: https://github.com/dotnet/runtime/issues/109757\n    -->\n    <LangVersion>13.0</LangVersion>\n    <Nullable>enable</Nullable>\n    <Version>0.5.1</Version>\n    <Platforms>AnyCPU;x64</Platforms>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\N_m3u8DL-RE.Parser\\N_m3u8DL-RE.Parser.csproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"System.CommandLine\" Version=\"2.0.0-rc.2.25502.107\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Processor/DemoProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Processor;\n\nnamespace N_m3u8DL_RE.Processor;\n\ninternal class DemoProcessor : ContentProcessor\n{\n\n    public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig)\n    {\n        return extractorType == ExtractorType.MPEG_DASH && parserConfig.Url.Contains(\"bitmovin\");\n    }\n\n    public override string Process(string rawText, ParserConfig parserConfig)\n    {\n        Logger.InfoMarkUp(\"[red]Match bitmovin![/]\");\n        return rawText;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Processor/DemoProcessor2.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Processor;\nusing N_m3u8DL_RE.Parser.Processor.HLS;\n\nnamespace N_m3u8DL_RE.Processor;\n\ninternal class DemoProcessor2 : KeyProcessor\n{\n    public override bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)\n    {\n        return extractorType == ExtractorType.HLS  && parserConfig.Url.Contains(\"playertest.longtailvideo.com\");\n    }\n\n    public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)\n    {\n        Logger.InfoMarkUp($\"[white on green]My Key Processor => {keyLine}[/]\");\n        var info = new DefaultHLSKeyProcessor().Process(keyLine, m3u8Url, m3u8Content, parserConfig);\n        Logger.InfoMarkUp(\"[red]\" + HexUtil.BytesToHex(info.Key!, \" \") + \"[/]\");\n        return info;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Processor/NowehoryzontyUrlProcessor.cs",
    "content": "﻿using System.Security.Cryptography;\nusing System.Text;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Processor;\nusing N_m3u8DL_RE.Parser.Util;\n\nnamespace N_m3u8DL_RE.Processor;\n\n// \"https://1429754964.rsc.cdn77.org/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/h264.mpd?secure=mSvVfvuciJt9wufUyzuBnA==,1658505709774\" --urlprocessor-args \"nowehoryzonty:timeDifference=-2274,filminfo.secureToken=vx54axqjal4f0yy2\"\n// \"https://1244073762.rsc.cdn77.org/r/ps19/ZAPOWIEDZI/CHCE_SPAC_TAK_BY_SNIC__ZAPOWIEDZ/19_10_25_0348_cgg/vp9.mpd?secure=_Xt_Kr6uVhYdZB64LMH5nQ==,1763158705542\" --urlprocessor-args \"nowehoryzonty:timeDifference=4,filminfo.secureToken=CDt7YToMQiv6RAGc\"\n\ninternal class NowehoryzontyUrlProcessor : UrlProcessor\n{\n    private const string ProcessorTag = \"nowehoryzonty:\";\n    \n    private static int _timeDifference;\n    private static string _secureToken = null!;\n    \n    private static bool _log;\n    \n    public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig)\n    {\n        if (extractorType == ExtractorType.MPEG_DASH && parserConfig.UrlProcessorArgs != null && parserConfig.UrlProcessorArgs.StartsWith(ProcessorTag)) \n        {\n            if (!_log)\n            {\n                Logger.WarnMarkUp(\"[white on green]www.nowehoryzonty.pl[/] matched!\");\n                _log = true;\n            }\n\n            var argLine = parserConfig.UrlProcessorArgs![ProcessorTag.Length..];\n            \n            _secureToken = ParserUtil.GetAttribute(argLine, \"filminfo.secureToken\")!;\n            _timeDifference = Convert.ToInt32(ParserUtil.GetAttribute(argLine, \"timeDifference\") ?? \"0\");\n            \n            return true;\n        }\n        \n        return false;\n    }\n\n    public override string Process(string oriUrl, ParserConfig parserConfig)\n    {\n        var path = new Uri(oriUrl).AbsolutePath;\n        return oriUrl + \"?secure=\" + Calc(path);\n    }\n\n    private static string Calc(string path)\n    {\n        var msTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 60000 + _timeDifference;\n\n        var hashPayload = msTime + path + _secureToken;\n        var hash = MD5.HashData(Encoding.UTF8.GetBytes(hashPayload));\n        var hashText = Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_');\n        \n        return $\"{hashText},{msTime}\";\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Program.cs",
    "content": "﻿using System.Globalization;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser;\nusing Spectre.Console;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common.Log;\nusing System.Text;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Processor;\nusing N_m3u8DL_RE.Config;\nusing N_m3u8DL_RE.Util;\nusing N_m3u8DL_RE.DownloadManager;\nusing N_m3u8DL_RE.CommandLine;\nusing System.Net;\nusing N_m3u8DL_RE.Enum;\n\nnamespace N_m3u8DL_RE;\n\ninternal class Program\n{\n    static async Task Main(string[] args)\n    {\n        // 处理NT6.0及以下System.CommandLine报错CultureNotFound问题\n        if (OperatingSystem.IsWindows()) \n        {\n            var osVersion = Environment.OSVersion.Version;\n            if (osVersion.Major < 6 || osVersion is { Major: 6, Minor: 0 })\n            {\n                Environment.SetEnvironmentVariable(\"DOTNET_SYSTEM_GLOBALIZATION_INVARIANT\", \"1\");\n            }\n        }\n        \n        Console.CancelKeyPress += Console_CancelKeyPress;\n        ServicePointManager.DefaultConnectionLimit = 1024;\n        try { Console.CursorVisible = true; } catch { }\n\n        string loc = CultureUtil.GetCurrentCultureName();\n\n        // 处理用户-h等请求\n        var index = -1;\n        var list = new List<string>(args);\n        if ((index = list.IndexOf(\"--ui-language\")) != -1 && list.Count > index + 1 && new List<string> { \"en-US\", \"zh-CN\", \"zh-TW\" }.Contains(list[index + 1]))\n        {\n            loc = list[index + 1];\n        }\n        \n        ResString.CurrentLoc = loc;\n\n        CultureUtil.ChangeCurrentCultureName(loc);\n\n        await CommandInvoker.InvokeArgs(args, DoWorkAsync);\n    }\n\n    private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)\n    {\n        Logger.WarnMarkUp(\"Force Exit...\");\n        try \n        { \n            Console.CursorVisible = true;\n            if (!OperatingSystem.IsWindows())\n                System.Diagnostics.Process.Start(\"tput\", \"cnorm\");\n        } catch { }\n        Environment.Exit(0);\n    }\n\n    static int GetOrder(StreamSpec streamSpec)\n    {\n        if (streamSpec.Channels == null) return 0;\n            \n        var str = streamSpec.Channels.Split('/')[0];\n        return int.TryParse(str, out var order) ? order : 0;\n    }\n\n    static async Task DoWorkAsync(MyOption option)\n    {\n        HTTPUtil.AppHttpClient.Timeout = TimeSpan.FromSeconds(option.HttpRequestTimeout);\n        if (Console.IsOutputRedirected || Console.IsErrorRedirected)\n        {\n            option.ForceAnsiConsole = true;\n            option.NoAnsiColor = true;\n            Logger.Info(ResString.consoleRedirected);\n        }\n        CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor);\n        \n        // 检测更新\n        if (!option.DisableUpdateCheck)\n            _ = CheckUpdateAsync();\n\n        Logger.IsWriteFile = !option.NoLog;\n        Logger.LogFilePath = option.LogFilePath;\n        Logger.InitLogFile();\n        Logger.LogLevel = option.LogLevel;\n        Logger.Info(CommandInvoker.VERSION_INFO);\n\n        if (option.UseSystemProxy == false)\n        {\n            HTTPUtil.HttpClientHandler.UseProxy = false;\n        }\n\n        if (option.CustomProxy != null)\n        {\n            HTTPUtil.HttpClientHandler.Proxy = option.CustomProxy;\n            HTTPUtil.HttpClientHandler.UseProxy = true;\n        }\n\n        // 检查互斥的选项\n        if (option is { MuxAfterDone: false, MuxImports.Count: > 0 })\n        {\n            throw new ArgumentException(\"MuxAfterDone disabled, MuxImports not allowed!\");\n        }\n\n        if (option.UseShakaPackager) \n        {\n            option.DecryptionEngine = DecryptEngine.SHAKA_PACKAGER;\n        }\n\n        // LivePipeMux开启时 LiveRealTimeMerge必须开启\n        if (option is { LivePipeMux: true, LiveRealTimeMerge: false })\n        {\n            Logger.WarnMarkUp(\"LivePipeMux detected, forced enable LiveRealTimeMerge\");\n            option.LiveRealTimeMerge = true;\n        }\n\n        // 预先检查ffmpeg\n        option.FFmpegBinaryPath ??= GlobalUtil.FindExecutable(\"ffmpeg\");\n\n        if (string.IsNullOrEmpty(option.FFmpegBinaryPath) || !File.Exists(option.FFmpegBinaryPath))\n        {\n            throw new FileNotFoundException(ResString.ffmpegNotFound);\n        }\n\n        Logger.Extra($\"ffmpeg => {option.FFmpegBinaryPath}\");\n\n        // 预先检查mkvmerge\n        if (option is { MuxOptions.UseMkvmerge: true, MuxAfterDone: true })\n        {\n            option.MkvmergeBinaryPath ??= GlobalUtil.FindExecutable(\"mkvmerge\");\n            if (string.IsNullOrEmpty(option.MkvmergeBinaryPath) || !File.Exists(option.MkvmergeBinaryPath))\n            {\n                throw new FileNotFoundException(ResString.mkvmergeNotFound);\n            }\n            Logger.Extra($\"mkvmerge => {option.MkvmergeBinaryPath}\");\n        }\n\n        // 预先检查\n        if (option.Keys is { Length: > 0 } || option.KeyTextFile != null)\n        {\n            if (!string.IsNullOrEmpty(option.DecryptionBinaryPath) && !File.Exists(option.DecryptionBinaryPath))\n            {\n                throw new FileNotFoundException(option.DecryptionBinaryPath);\n            }\n            switch (option.DecryptionEngine)\n            {\n                case DecryptEngine.SHAKA_PACKAGER:\n                {\n                    var file = GlobalUtil.FindExecutable(\"shaka-packager\");\n                    var file2 = GlobalUtil.FindExecutable(\"packager-linux-x64\");\n                    var file3 = GlobalUtil.FindExecutable(\"packager-osx-x64\");\n                    var file4 = GlobalUtil.FindExecutable(\"packager-win-x64\");\n                    if (file == null && file2 == null && file3 == null && file4 == null)\n                        throw new FileNotFoundException(ResString.shakaPackagerNotFound);\n                    option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;\n                    Logger.Extra($\"shaka-packager => {option.DecryptionBinaryPath}\");\n                    break;\n                }\n                case DecryptEngine.MP4DECRYPT:\n                {\n                    var file = GlobalUtil.FindExecutable(\"mp4decrypt\");\n                    if (file == null) throw new FileNotFoundException(ResString.mp4decryptNotFound);\n                    option.DecryptionBinaryPath = file;\n                    Logger.Extra($\"mp4decrypt => {option.DecryptionBinaryPath}\");\n                    break;\n                }\n                case DecryptEngine.FFMPEG:\n                default:\n                    option.DecryptionBinaryPath = option.FFmpegBinaryPath;\n                    break;\n            }\n        }\n\n        // 默认的headers\n        var headers = new Dictionary<string, string>()\n        {\n            [\"user-agent\"] = \"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\"\n        };\n        // 添加或替换用户输入的headers\n        foreach (var item in option.Headers)\n        {\n            headers[item.Key] = item.Value;\n            Logger.Extra($\"User-Defined Header => {item.Key}: {item.Value}\");\n        }\n\n        var parserConfig = new ParserConfig()\n        {\n            AppendUrlParams = option.AppendUrlParams,\n            UrlProcessorArgs = option.UrlProcessorArgs,\n            BaseUrl = option.BaseUrl!,\n            Headers = headers,\n            CustomMethod = option.CustomHLSMethod,\n            CustomeKey = option.CustomHLSKey,\n            CustomeIV = option.CustomHLSIv,\n        };\n\n        if (option.AllowHlsMultiExtMap)\n        {\n            parserConfig.CustomParserArgs.Add(\"AllowHlsMultiExtMap\", \"true\");\n        }\n\n        // demo1\n        parserConfig.ContentProcessors.Insert(0, new DemoProcessor());\n        // demo2\n        parserConfig.KeyProcessors.Insert(0, new DemoProcessor2());\n        // for www.nowehoryzonty.pl\n        parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());\n\n        // 等待任务开始时间\n        if (option.TaskStartAt != null && option.TaskStartAt > DateTime.Now)\n        {\n            Logger.InfoMarkUp(ResString.taskStartAt + option.TaskStartAt);\n            while (option.TaskStartAt > DateTime.Now)\n            {\n                await Task.Delay(1000);\n            }\n        }\n\n        var url = option.Input;\n\n        // 流提取器配置\n        var extractor = new StreamExtractor(parserConfig);\n        // 从链接加载内容\n        await RetryUtil.WebRequestRetryAsync(async () =>\n        {\n            await extractor.LoadSourceFromUrlAsync(url);\n            return true;\n        });\n        // 解析流信息\n        var streams = await extractor.ExtractStreamsAsync();\n\n\n        // 全部媒体\n        var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder).ToList();\n        // 基本流\n        var basicStreams = lists.Where(x => x.MediaType is null or MediaType.VIDEO).ToList();\n        // 可选音频轨道\n        var audios = lists.Where(x => x.MediaType == MediaType.AUDIO).ToList();\n        // 可选字幕轨道\n        var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES).ToList();\n\n        // 尝试从URL或文件读取文件名\n        if (string.IsNullOrEmpty(option.SaveName))\n        {\n            option.SaveName = OtherUtil.GetFileNameFromInput(option.Input);\n        }\n\n        // 生成文件夹\n        var tmpDir = Path.Combine(option.TmpDir ?? Environment.CurrentDirectory, $\"{option.SaveName ?? DateTime.Now.ToString(\"yyyy-MM-dd_HH-mm-ss\")}\");\n        // 记录文件\n        if (option.WriteMetaJson)\n        {\n            extractor.RawFiles[\"meta.json\"] = GlobalUtil.ConvertToJson(lists);\n        }\n        // 写出文件\n        await WriteRawFilesAsync(option, extractor, tmpDir);\n\n        Logger.Info(ResString.streamsInfo, lists.Count, basicStreams.Count, audios.Count, subs.Count);\n\n        foreach (var item in lists)\n        {\n            Logger.InfoMarkUp(item.ToString());\n        }\n\n        var selectedStreams = new List<StreamSpec>();\n        if (option.DropVideoFilter != null || option.DropAudioFilter != null || option.DropSubtitleFilter != null)\n        {\n            basicStreams = FilterUtil.DoFilterDrop(basicStreams, option.DropVideoFilter);\n            audios = FilterUtil.DoFilterDrop(audios, option.DropAudioFilter);\n            subs = FilterUtil.DoFilterDrop(subs, option.DropSubtitleFilter);\n            lists = basicStreams.Concat(audios).Concat(subs).ToList();\n        }\n\n        if (option.DropVideoFilter != null) Logger.Extra($\"DropVideoFilter => {option.DropVideoFilter}\");\n        if (option.DropAudioFilter != null) Logger.Extra($\"DropAudioFilter => {option.DropAudioFilter}\");\n        if (option.DropSubtitleFilter != null) Logger.Extra($\"DropSubtitleFilter => {option.DropSubtitleFilter}\");\n        if (option.VideoFilter != null) Logger.Extra($\"VideoFilter => {option.VideoFilter}\");\n        if (option.AudioFilter != null) Logger.Extra($\"AudioFilter => {option.AudioFilter}\");\n        if (option.SubtitleFilter != null) Logger.Extra($\"SubtitleFilter => {option.SubtitleFilter}\");\n\n        if (option.AutoSelect)\n        {\n            if (basicStreams.Count != 0)\n                selectedStreams.Add(basicStreams.First());\n            var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);\n            foreach (var lang in langs)\n            {\n                selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).ThenByDescending(GetOrder).First());\n            }\n            selectedStreams.AddRange(subs);\n        }\n        else if (option.SubOnly)\n        {\n            selectedStreams.AddRange(subs);\n        }\n        else if (option.VideoFilter != null || option.AudioFilter != null || option.SubtitleFilter != null)\n        {\n            basicStreams = FilterUtil.DoFilterKeep(basicStreams, option.VideoFilter);\n            audios = FilterUtil.DoFilterKeep(audios, option.AudioFilter);\n            subs = FilterUtil.DoFilterKeep(subs, option.SubtitleFilter);\n            selectedStreams = basicStreams.Concat(audios).Concat(subs).ToList();\n        }\n        else\n        {\n            // 展示交互式选择框\n            selectedStreams = FilterUtil.SelectStreams(lists);\n        }\n\n        if (selectedStreams.Count == 0)\n            throw new Exception(ResString.noStreamsToDownload);\n\n        // HLS: 选中流中若有没加载出playlist的，加载playlist\n        // DASH/MSS: 加载playlist (调用url预处理器)\n        if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS)\n            await extractor.FetchPlayListAsync(selectedStreams);\n\n        // 直播检测\n        var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod;\n        if (livingFlag)\n        {\n            Logger.WarnMarkUp($\"[white on darkorange3_1]{ResString.liveFound}[/]\");\n        }\n\n        // 无法识别的加密方式，自动开启二进制合并\n        if (selectedStreams.Any(s => s.Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN))))\n        {\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge3}[/]\");\n            option.BinaryMerge = true;\n        }\n\n        // 应用用户自定义的分片范围\n        if (!livingFlag)\n            FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange);\n\n        // 应用用户自定义的广告分片关键字\n        FilterUtil.CleanAd(selectedStreams, option.AdKeywords);\n\n        // 记录文件\n        if (option.WriteMetaJson)\n        {\n            extractor.RawFiles[\"meta_selected.json\"] = GlobalUtil.ConvertToJson(selectedStreams);\n        }\n\n        Logger.Info(ResString.selectedStream);\n        foreach (var item in selectedStreams)\n        {\n            Logger.InfoMarkUp(item.ToString());\n        }\n\n        // 写出文件\n        await WriteRawFilesAsync(option, extractor, tmpDir);\n\n        if (option.SkipDownload)\n        {\n            return;\n        }\n\n#if DEBUG\n        Console.WriteLine(\"Press any key to continue...\");\n        Console.ReadKey();\n#endif\n\n        Logger.InfoMarkUp(ResString.saveName + $\"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]\");\n\n        // 开始MuxAfterDone后自动使用二进制版\n        if (option is { BinaryMerge: false, MuxAfterDone: true })\n        {\n            option.BinaryMerge = true;\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.autoBinaryMerge6}[/]\");\n        }\n\n        // 下载配置\n        var downloadConfig = new DownloaderConfig()\n        {\n            MyOptions = option,\n            DirPrefix = tmpDir,\n            Headers = parserConfig.Headers, // 使用命令行解析得到的Headers\n        };\n\n        var result = false;\n\n        if (extractor.ExtractorType == ExtractorType.HTTP_LIVE)\n        {\n            var sldm = new HTTPLiveRecordManager(downloadConfig, selectedStreams, extractor);\n            result = await sldm.StartRecordAsync();\n        }\n        else if (!livingFlag)\n        {\n            // 开始下载\n            var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor);\n            result = await sdm.StartDownloadAsync();\n        }\n        else\n        {\n            var sldm = new SimpleLiveRecordManager2(downloadConfig, selectedStreams, extractor);\n            result = await sldm.StartRecordAsync();\n        }\n\n        if (result)\n        {\n            Logger.InfoMarkUp(\"[white on green]Done[/]\");\n        }\n        else\n        {\n            Logger.ErrorMarkUp(\"[white on red]Failed[/]\");\n            Environment.ExitCode = 1;\n        }\n    }\n\n    private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir)\n    {\n        // 写出json文件\n        if (option.WriteMetaJson)\n        {\n            if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);\n            Logger.Warn(ResString.writeJson);\n            foreach (var item in extractor.RawFiles)\n            {\n                var file = Path.Combine(tmpDir, item.Key);\n                if (!File.Exists(file)) await File.WriteAllTextAsync(file, item.Value, Encoding.UTF8);\n            }\n        }\n    }\n\n    static async Task CheckUpdateAsync()\n    {\n        try\n        {\n            var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!;\n            string nowVer = $\"v{ver.Major}.{ver.Minor}.{ver.Build}\";\n            string redirctUrl = await Get302Async(\"https://github.com/nilaoda/N_m3u8DL-RE/releases/latest\");\n            string latestVer = redirctUrl.Replace(\"https://github.com/nilaoda/N_m3u8DL-RE/releases/tag/\", \"\");\n            if (!latestVer.StartsWith(nowVer) && !latestVer.StartsWith(\"https\"))\n            {\n                Console.Title = $\"{ResString.newVersionFound} {latestVer}\";\n                Logger.InfoMarkUp($\"[cyan]{ResString.newVersionFound}[/] [red]{latestVer}[/]\");\n            }\n        }\n        catch (Exception)\n        {\n            ;\n        }\n    }\n\n    // 重定向\n    static async Task<string> Get302Async(string url)\n    {\n        // this allows you to set the settings so that we can get the redirect url\n        var handler = new HttpClientHandler\n        {\n            AllowAutoRedirect = false\n        };\n        var redirectedUrl = \"\";\n        using var client = new HttpClient(handler);\n        using var response = await client.GetAsync(url);\n        using var content = response.Content;\n        // ... Read the response to see if we have the redirected url\n        if (response.StatusCode != HttpStatusCode.Found) return redirectedUrl;\n        \n        var headers = response.Headers;\n        if (headers.Location != null)\n        {\n            redirectedUrl = headers.Location.AbsoluteUri;\n        }\n\n        return redirectedUrl;\n    }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/CultureUtil.cs",
    "content": "using N_m3u8DL_RE.Common.Resource;\n\nnamespace N_m3u8DL_RE.Util;\n\npublic static class CultureUtil\n{\n    public static string GetCurrentCultureName()\n    {\n        string loc = ResString.CurrentLoc;\n        string currLoc = Thread.CurrentThread.CurrentUICulture.Name;\n        \n        if (string.IsNullOrEmpty(currLoc))\n            currLoc = GetCurrentCultureNameFromEnvironment();\n        \n        if (currLoc is \"zh-CN\" or \"zh-SG\")\n            loc = \"zh-CN\";\n        else if (currLoc.StartsWith(\"zh-\"))\n            loc = \"zh-TW\";\n        \n        return loc;\n    }\n\n    public static void ChangeCurrentCultureName(string newName)\n    {\n        try\n        {\n            System.Globalization.CultureInfo.DefaultThreadCurrentCulture = new System.Globalization.CultureInfo(newName);\n            Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(newName);\n            Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo(newName);\n        }\n        catch (Exception)\n        {\n            // Culture not work on NT6.0, so catch the exception\n        }\n    }\n\n    private static string GetCurrentCultureNameFromEnvironment()\n    {\n        // 尝试读取 LC_ALL, LANG\n        string langEnv = Environment.GetEnvironmentVariable(\"LC_ALL\") \n                         ?? Environment.GetEnvironmentVariable(\"LANG\") \n                         ?? ResString.CurrentLoc;\n        return langEnv.Split('.')[0].Replace('_', '-');\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/DownloadUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Entity;\nusing System.Net.Http.Headers;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static class DownloadUtil\n{\n    private static readonly HttpClient AppHttpClient = HTTPUtil.AppHttpClient;\n\n    private static async Task<DownloadResult> CopyFileAsync(string sourceFile, string path, SpeedContainer speedContainer, long? fromPosition = null, long? toPosition = null)\n    {\n        using var inputStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read);\n        using var outputStream = new FileStream(path, FileMode.OpenOrCreate);\n        inputStream.Seek(fromPosition ?? 0L, SeekOrigin.Begin);\n        var expect = (toPosition ?? inputStream.Length) - inputStream.Position + 1;\n        if (expect == inputStream.Length + 1)\n        {\n            await inputStream.CopyToAsync(outputStream);\n            speedContainer.Add(inputStream.Length);\n        }\n        else\n        {\n            var buffer = new byte[expect];\n            _ = await inputStream.ReadAsync(buffer);\n            await outputStream.WriteAsync(buffer);\n            speedContainer.Add(buffer.Length);\n        }\n        return new DownloadResult()\n        {\n            ActualContentLength = outputStream.Length,\n            ActualFilePath = path\n        };\n    }\n\n    public static async Task<DownloadResult> DownloadToFileAsync(string url, string path, SpeedContainer speedContainer, CancellationTokenSource cancellationTokenSource, Dictionary<string, string>? headers = null, long? fromPosition = null, long? toPosition = null)\n    {\n        Logger.Debug(ResString.fetch + url);\n        if (url.StartsWith(\"file:\"))\n        {\n            var file = new Uri(url).LocalPath;\n            return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition);\n        }\n        if (url.StartsWith(\"base64://\"))\n        {\n            var bytes = Convert.FromBase64String(url[9..]);\n            await File.WriteAllBytesAsync(path, bytes);\n            return new DownloadResult()\n            {\n                ActualContentLength = bytes.Length,\n                ActualFilePath = path,\n            };\n        }\n        if (url.StartsWith(\"hex://\"))\n        {\n            var bytes = HexUtil.HexToBytes(url[6..]);\n            await File.WriteAllBytesAsync(path, bytes);\n            return new DownloadResult()\n            {\n                ActualContentLength = bytes.Length,\n                ActualFilePath = path,\n            };\n        }\n        using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));\n        if (fromPosition != null || toPosition != null)\n            request.Headers.Range = new(fromPosition, toPosition);\n        if (headers != null)\n        {\n            foreach (var item in headers)\n            {\n                request.Headers.TryAddWithoutValidation(item.Key, item.Value);\n            }\n        }\n        Logger.Debug(request.Headers.ToString());\n        try\n        {\n            using var response = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token);\n            if (((int)response.StatusCode).ToString().StartsWith(\"30\"))\n            {\n                HttpResponseHeaders respHeaders = response.Headers;\n                Logger.Debug(respHeaders.ToString());\n                if (respHeaders.Location != null)\n                {\n                    var redirectedUrl = \"\";\n                    if (!respHeaders.Location.IsAbsoluteUri)\n                    {\n                        Uri uri1 = new Uri(url);\n                        Uri uri2 = new Uri(uri1, respHeaders.Location);\n                        redirectedUrl = uri2.ToString();\n                    }\n                    else\n                    {\n                        redirectedUrl = respHeaders.Location.AbsoluteUri;\n                    }\n                    return await DownloadToFileAsync(redirectedUrl, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition);\n                }\n            }\n            response.EnsureSuccessStatusCode();\n            var contentLength = response.Content.Headers.ContentLength;\n            if (speedContainer.SingleSegment) speedContainer.ResponseLength = contentLength;\n\n            using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);\n            using var responseStream = await response.Content.ReadAsStreamAsync(cancellationTokenSource.Token);\n            var buffer = new byte[16 * 1024];\n            var size = 0;\n\n            size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);\n            speedContainer.Add(size);\n            await stream.WriteAsync(buffer.AsMemory(0, size));\n            // 检测imageHeader\n            bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);\n            // 检测GZip（For DDP Audio）\n            bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;\n\n            while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)\n            {\n                speedContainer.Add(size);\n                await stream.WriteAsync(buffer.AsMemory(0, size));\n                // 限速策略\n                while (speedContainer.Downloaded > speedContainer.SpeedLimit)\n                {\n                    await Task.Delay(1);\n                }\n            }\n\n            return new DownloadResult()\n            {\n                ActualContentLength = stream.Length,\n                RespContentLength = contentLength,\n                ActualFilePath = path,\n                ImageHeader= imageHeader,\n                GzipHeader = gZipHeader\n            };\n        }\n        catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationTokenSource.Token)\n        {\n            speedContainer.ResetLowSpeedCount();\n            throw new Exception(\"Download speed too slow!\");\n        }\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/FilterUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Entity;\nusing Spectre.Console;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Util;\n\npublic static class FilterUtil\n{\n    public static List<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> lists, StreamFilter? filter)\n    {\n        if (filter == null) return [];\n\n        var inputs = lists.Where(_ => true);\n        if (filter.GroupIdReg != null)\n            inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId));\n        if (filter.LanguageReg != null)\n            inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language));\n        if (filter.NameReg != null)\n            inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name));\n        if (filter.CodecsReg != null)\n            inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs));\n        if (filter.ResolutionReg != null)\n            inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution));\n        if (filter.FrameRateReg != null)\n            inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($\"{i.FrameRate}\"));\n        if (filter.ChannelsReg != null)\n            inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels));\n        if (filter.VideoRangeReg != null)\n            inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange));\n        if (filter.UrlReg != null)\n            inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url));\n        if (filter.SegmentsMaxCount != null && inputs.All(i => i.SegmentsCount > 0)) \n            inputs = inputs.Where(i => i.SegmentsCount < filter.SegmentsMaxCount);\n        if (filter.SegmentsMinCount != null && inputs.All(i => i.SegmentsCount > 0))\n            inputs = inputs.Where(i => i.SegmentsCount > filter.SegmentsMinCount);\n        if (filter.PlaylistMinDur != null)\n            inputs = inputs.Where(i => i.Playlist?.TotalDuration > filter.PlaylistMinDur);\n        if (filter.PlaylistMaxDur != null)\n            inputs = inputs.Where(i => i.Playlist?.TotalDuration < filter.PlaylistMaxDur);\n        if (filter.BandwidthMin != null)\n            inputs = inputs.Where(i => i.Bandwidth >= filter.BandwidthMin);\n        if (filter.BandwidthMax != null)\n            inputs = inputs.Where(i => i.Bandwidth <= filter.BandwidthMax);\n        if (filter.Role.HasValue)\n            inputs = inputs.Where(i => i.Role == filter.Role);\n\n        var bestNumberStr = filter.For.Replace(\"best\", \"\");\n        var worstNumberStr = filter.For.Replace(\"worst\", \"\");\n\n        if (filter.For == \"best\" && inputs.Any())\n            inputs = inputs.Take(1).ToList();\n        else if (filter.For == \"worst\" && inputs.Any())\n            inputs = inputs.TakeLast(1).ToList();\n        else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Any())\n            inputs = inputs.Take(bestNumber).ToList();\n        else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Any())\n            inputs = inputs.TakeLast(worstNumber).ToList();\n\n        return inputs.ToList();\n    }\n\n    public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)\n    {\n        if (filter == null) return [..lists];\n\n        var inputs = lists.Where(_ => true);\n        var selected = DoFilterKeep(lists, filter);\n\n        inputs = inputs.Where(i => selected.All(s => s.ToString() != i.ToString()));\n\n        return inputs.ToList();\n    }\n\n    public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)\n    {\n        var streamSpecs = lists.ToList();\n        if (streamSpecs.Count == 1)\n            return [..streamSpecs];\n\n        // 基本流\n        var basicStreams = streamSpecs.Where(x => x.MediaType == null).ToList();\n        // 可选音频轨道\n        var audios = streamSpecs.Where(x => x.MediaType == MediaType.AUDIO).ToList();\n        // 可选字幕轨道\n        var subs = streamSpecs.Where(x => x.MediaType == MediaType.SUBTITLES).ToList();\n\n        var prompt = new MultiSelectionPrompt<StreamSpec>()\n                .Title(ResString.promptTitle)\n                .UseConverter(x =>\n                {\n                    if (x.Name != null && x.Name.StartsWith(\"__\"))\n                        return $\"[darkslategray1]{x.Name[2..]}[/]\";\n                    return x.ToString().EscapeMarkup().RemoveMarkup();\n                })\n                .Required()\n                .PageSize(10)\n                .MoreChoicesText(ResString.promptChoiceText)\n                .InstructionsText(ResString.promptInfo)\n            ;\n\n        // 默认选中第一个\n        var first = streamSpecs.First();\n        prompt.Select(first);\n\n        if (basicStreams.Count != 0)\n        {\n            prompt.AddChoiceGroup(new StreamSpec() { Name = \"__Basic\" }, basicStreams);\n        }\n\n        if (audios.Count != 0)\n        {\n            prompt.AddChoiceGroup(new StreamSpec() { Name = \"__Audio\" }, audios);\n            // 默认音轨\n            if (first.AudioId != null)\n            {\n                prompt.Select(audios.First(a => a.GroupId == first.AudioId));\n            }\n        }\n        if (subs.Count != 0)\n        {\n            prompt.AddChoiceGroup(new StreamSpec() { Name = \"__Subtitle\" }, subs);\n            // 默认字幕轨\n            if (first.SubtitleId != null)\n            {\n                prompt.Select(subs.First(s => s.GroupId == first.SubtitleId));\n            }\n        }\n\n        // 如果此时还是没有选中任何流，自动选择一个\n        prompt.Select(basicStreams.Concat(audios).Concat(subs).First());\n\n        // 多选\n        var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt);\n\n        return selectedStreams;\n    }\n\n    /// <summary>\n    /// 直播使用。对齐各个轨道的起始。\n    /// </summary>\n    /// <param name=\"selectedSteams\"></param>\n    /// <param name=\"takeLastCount\"></param>\n    public static void SyncStreams(List<StreamSpec> selectedSteams, int takeLastCount = 15)\n    {\n        // 过滤出需要处理的流\n        selectedSteams = selectedSteams\n            .Where(s => s.Playlist?.MediaParts?.Any(p => p.MediaSegments.Count != 0) == true)\n            .ToList();\n        // 通过Date同步\n        if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null)))\n        {\n            var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!;\n            foreach (var item in selectedSteams)\n            {\n                foreach (var part in item.Playlist!.MediaParts)\n                {\n                    // 秒级同步 忽略毫秒\n                    part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList();\n                }\n            }\n        }\n        else // 通过index同步\n        {\n            var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index));\n            foreach (var item in selectedSteams)\n            {\n                foreach (var part in item.Playlist!.MediaParts)\n                {\n                    part.MediaSegments = part.MediaSegments.Where(s => s.Index >= minIndex).ToList();\n                }\n            }\n        }\n\n        // 取最新的N个分片\n        if (selectedSteams.Any(x => x.Playlist!.MediaParts[0].MediaSegments.Count > takeLastCount))\n        {\n            var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1;\n            if (skipCount < 0) skipCount = 0;\n            foreach (var item in selectedSteams)\n            {\n                foreach (var part in item.Playlist!.MediaParts)\n                {\n                    part.MediaSegments = part.MediaSegments.Skip(skipCount).ToList();\n                }\n            }\n        }\n    }\n\n    /// <summary>\n    /// 应用用户自定义的分片范围\n    /// </summary>\n    /// <param name=\"selectedSteams\"></param>\n    /// <param name=\"customRange\"></param>\n    public static void ApplyCustomRange(List<StreamSpec> selectedSteams, CustomRange? customRange)\n    {\n        if (customRange == null) return;\n\n        Logger.InfoMarkUp($\"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]\");\n        Logger.WarnMarkUp($\"[darkorange3_1]{ResString.customRangeWarn}[/]\");\n\n        var filterByIndex = customRange is { StartSegIndex: not null, EndSegIndex: not null };\n        var filterByTime = customRange is { StartSec: not null, EndSec: not null };\n\n        if (!filterByIndex && !filterByTime)\n        {\n            Logger.ErrorMarkUp(ResString.customRangeInvalid);\n            return;\n        }\n\n        foreach (var stream in selectedSteams)\n        {\n            var skippedDur = 0d;\n            if (stream.Playlist == null) continue;\n            foreach (var part in stream.Playlist.MediaParts)\n            {\n                List<MediaSegment> newSegments;\n                if (filterByIndex)\n                    newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList();\n                else\n                    newSegments = part.MediaSegments.Where(seg => stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) >= customRange.StartSec\n                                                                  && stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) <= customRange.EndSec).ToList();\n\n                if (newSegments.Count > 0)\n                    skippedDur += part.MediaSegments.Where(seg => seg.Index < newSegments.First().Index).Sum(x => x.Duration);\n                part.MediaSegments = newSegments;\n            }\n            stream.SkippedDuration = skippedDur;\n        }\n    }\n\n    /// <summary>\n    /// 根据用户输入，清除广告分片\n    /// </summary>\n    /// <param name=\"selectedSteams\"></param>\n    /// <param name=\"keywords\"></param>\n    public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords)\n    {\n        if (keywords == null) return;\n        var regList = keywords.Select(s => new Regex(s)).ToList();\n        foreach ( var reg in regList)\n        {\n            Logger.InfoMarkUp($\"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]\");\n        }\n\n        foreach (var stream in selectedSteams)\n        {\n            if (stream.Playlist == null) continue;\n\n            var countBefore = stream.SegmentsCount;\n\n            foreach (var part in stream.Playlist.MediaParts)\n            {\n                // 没有找到广告分片\n                if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url))))\n                {\n                    continue;\n                }\n                // 找到广告分片 清理\n                part.MediaSegments = part.MediaSegments.Where(x => regList.All(reg => !reg.IsMatch(x.Url))).ToList();\n            }\n\n            // 清理已经为空的 part\n            stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList();\n\n            var countAfter = stream.SegmentsCount;\n\n            if (countBefore != countAfter)\n            {\n                Logger.WarnMarkUp(\"[grey]{} segments => {} segments[/]\", countBefore, countAfter);\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/ImageHeaderUtil.cs",
    "content": "namespace N_m3u8DL_RE.Util;\n\ninternal static class ImageHeaderUtil\n{\n    public static bool IsImageHeader(byte[] bArr)\n    {\n        var size = bArr.Length;\n        // PNG HEADER检测\n        if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3])\n            return true;\n        // GIF HEADER检测\n        if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3])\n            return true;\n        // BMP HEADER检测\n        if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8])\n            return true;\n        // JPEG HEADER检测\n        if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2])\n            return true;\n        return false;\n    }\n\n    public static async Task ProcessAsync(string sourcePath)\n    {\n        var sourceData = await File.ReadAllBytesAsync(sourcePath);\n\n        // PNG HEADER\n        if (137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3])\n        {\n            if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119])\n                sourceData = sourceData[120..];\n            else if (sourceData.Length > 6102 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[6100] && 130 == sourceData[6101])\n                sourceData = sourceData[6102..];\n            else if (sourceData.Length > 69 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[67] && 130 == sourceData[68])\n                sourceData = sourceData[69..];\n            else if (sourceData.Length > 771 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[769] && 130 == sourceData[770])\n                sourceData = sourceData[771..];\n            else\n            {\n                // 手动查询结尾标记 0x47 出现两次\n                int skip = 0;\n                for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)\n                {\n                    if (sourceData[i] == 0x47 && sourceData[i + 188] == 0x47 && sourceData[i + 188 + 188] == 0x47)\n                    {\n                        skip = i;\n                        break;\n                    }\n                }\n                sourceData = sourceData[skip..];\n            }\n        }\n        // GIF HEADER\n        else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3])\n        {\n            sourceData = sourceData[42..];\n        }\n        // BMP HEADER\n        else if (0x42 == sourceData[0] && 0x4D == sourceData[1] && 0x00 == sourceData[5] && 0x00 == sourceData[6] && 0x00 == sourceData[7] && 0x00 == sourceData[8])\n        {\n            sourceData = sourceData[0x3E..];\n        }\n        // JPEG HEADER检测\n        else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2])\n        {\n            // 手动查询结尾标记 0x47 出现两次\n            int skip = 0;\n            for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)\n            {\n                if (sourceData[i] == 0x47 && sourceData[i + 188] == 0x47 && sourceData[i + 188 + 188] == 0x47)\n                {\n                    skip = i;\n                    break;\n                }\n            }\n            sourceData = sourceData[skip..];\n        }\n\n        await File.WriteAllBytesAsync(sourcePath, sourceData);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/LanguageCodeUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Entity;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal class Language(string extendCode, string code, string desc, string descA)\n{\n    public readonly string Code = code;\n    public readonly string ExtendCode = extendCode;\n    public readonly string Description = desc;\n    public readonly string DescriptionAudio = descA;\n}\n\ninternal static class LanguageCodeUtil\n{\n\n    private static readonly List<Language> ALL_LANGS = @\"\ndefault;und;default;default\naf;afr;Afrikaans;Afrikaans\naf-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa)\nam;amh;Amharic;Amharic\nam-ET;amh;Amharic (Ethiopia);Amharic (Ethiopia)\nar;ara;Arabic;Arabic\nar-SA;ara;Arabic (Saudi Arabia);Arabic (Saudi Arabia)\nar-IQ;ara;Arabic (Iraq);Arabic (Iraq)\nar-EG;ara;Arabic (Egypt);Arabic (Egypt)\nar-LY;ara;Arabic (Libya);Arabic (Libya)\nar-DZ;ara;Arabic (Algeria);Arabic (Algeria)\nar-MA;ara;Arabic (Morocco);Arabic (Morocco)\nar-TN;ara;Arabic (Tunisia);Arabic (Tunisia)\nar-OM;ara;Arabic (Oman);Arabic (Oman)\nar-YE;ara;Arabic (Yemen);Arabic (Yemen)\nar-SY;ara;Arabic (Syria);Arabic (Syria)\nar-JO;ara;Arabic (Jordan);Arabic (Jordan)\nar-LB;ara;Arabic (Lebanon);Arabic (Lebanon)\nar-KW;ara;Arabic (Kuwait);Arabic (Kuwait)\nar-AE;ara;Arabic (United Arab Emirates);Arabic (United Arab Emirates)\nar-BH;ara;Arabic (Bahrain);Arabic (Bahrain)\nar-QA;ara;Arabic (Qatar);Arabic (Qatar)\nas;asm;Assamese;Assamese\nas-IN;asm;Assamese (India);Assamese (India)\naz;aze;Azerbaijani;Azerbaijani\naz-Latn-AZ;aze;Azerbaijani (Latin, Azerbaijan);Azerbaijani (Latin, Azerbaijan)\naz-Cyrl-AZ;aze;Azerbaijani (Cyrillic, Azerbaijan);Azerbaijani (Cyrillic, Azerbaijan)\naz-Cyrl;aze;Azerbaijani (Cyrillic);Azerbaijani (Cyrillic)\naz-Latn;aze;Azerbaijani (Latin);Azerbaijani (Latin)\nbe;bel;Belarusian;Belarusian\nbe-BY;bel;Belarusian (Belarus);Belarusian (Belarus)\nbg;bul;Bulgarian;Bulgarian\nbg-BG;bul;Bulgarian (Bulgaria);Bulgarian (Bulgaria)\nbn;ben;Bangla;Bangla\nbn-IN;ben;Bangla (India);Bangla (India)\nbn-BD;ben;Bangla (Bangladesh);Bangla (Bangladesh)\nbo;bod;Tibetan;Tibetan\nbo-CN;bod;Tibetan (China);Tibetan (China)\nbr;bre;Breton;Breton\nbr-FR;bre;Breton (France);Breton (France)\nbs-Latn-BA;bos;Bosnian (Latin, Bosnia & Herzegovina);Bosnian (Latin, Bosnia & Herzegovina)\nbs-Cyrl-BA;bos;Bosnian (Cyrillic, Bosnia & Herzegovina);Bosnian (Cyrillic, Bosnia & Herzegovina)\nbs-Cyrl;bos;Bosnian (Cyrillic);Bosnian (Cyrillic)\nbs-Latn;bos;Bosnian (Latin);Bosnian (Latin)\nbs;bos;Bosnian;Bosnian\nca;cat;Catalan;Catalan\nca-ES;cat;Catalan (Spain);Catalan (Spain)\nca-ES-valencia;cat;Catalan (Spain);Catalan (Spain)\nchr;chr;Cherokee;Cherokee\ncs;ces;Czech;Czech\ncs-CZ;ces;Czech (Czech Republic);Czech (Czech Republic)\ncy;cym;Welsh;Welsh\ncy-GB;cym;Welsh (United Kingdom);Welsh (United Kingdom)\nda;dan;Danish;Danish\nda-DK;dan;Danish (Denmark);Danish (Denmark)\nde;deu;German;German\nde-DE;deu;German (Germany);German (Germany)\nde-CH;deu;German (Switzerland);German (Switzerland)\nde-AT;deu;German (Austria);German (Austria)\nde-LU;deu;German (Luxembourg);German (Luxembourg)\nde-LI;deu;German (Liechtenstein);German (Liechtenstein)\ndsb-DE;dsb;Lower Sorbian (Germany);Lower Sorbian (Germany)\ndsb;dsb;Lower Sorbian;Lower Sorbian\nel;ell;Greek;Greek\nel-GR;ell;Greek (Greece);Greek (Greece)\nen;eng;English;English\nen-US;eng;English (United States);English (United States)\nen-GB;eng;English (United Kingdom);English (United Kingdom)\nen-AU;eng;English (Australia);English (Australia)\nen-CA;eng;English (Canada);English (Canada)\nen-NZ;eng;English (New Zealand);English (New Zealand)\nen-IE;eng;English (Ireland);English (Ireland)\nen-ZA;eng;English (South Africa);English (South Africa)\nen-JM;eng;English (Jamaica);English (Jamaica)\nen-BZ;eng;English (Belize);English (Belize)\nen-TT;eng;English (Trinidad & Tobago);English (Trinidad & Tobago)\nen-ZW;eng;English (Zimbabwe);English (Zimbabwe)\nen-PH;eng;English (Philippines);English (Philippines)\nen-HK;eng;English (Hong Kong SAR China);English (Hong Kong SAR China)\nen-IN;eng;English (India);English (India)\nen-MY;eng;English (Malaysia);English (Malaysia)\nen-SG;eng;English (Singapore);English (Singapore)\nes;spa;Spanish;Spanish\nes-MX;spa;Spanish (Mexico);Spanish (Mexico)\nes-ES;spa;Spanish (Spain);Spanish (Spain)\nes-GT;spa;Spanish (Guatemala);Spanish (Guatemala)\nes-CR;spa;Spanish (Costa Rica);Spanish (Costa Rica)\nes-PA;spa;Spanish (Panama);Spanish (Panama)\nes-DO;spa;Spanish (Dominican Republic);Spanish (Dominican Republic)\nes-VE;spa;Spanish (Venezuela);Spanish (Venezuela)\nes-CO;spa;Spanish (Colombia);Spanish (Colombia)\nes-PE;spa;Spanish (Peru);Spanish (Peru)\nes-AR;spa;Spanish (Argentina);Spanish (Argentina)\nes-EC;spa;Spanish (Ecuador);Spanish (Ecuador)\nes-CL;spa;Spanish (Chile);Spanish (Chile)\nes-UY;spa;Spanish (Uruguay);Spanish (Uruguay)\nes-PY;spa;Spanish (Paraguay);Spanish (Paraguay)\nes-BO;spa;Spanish (Bolivia);Spanish (Bolivia)\nes-SV;spa;Spanish (El Salvador);Spanish (El Salvador)\nes-HN;spa;Spanish (Honduras);Spanish (Honduras)\nes-NI;spa;Spanish (Nicaragua);Spanish (Nicaragua)\nes-PR;spa;Spanish (Puerto Rico);Spanish (Puerto Rico)\nes-US;spa;Spanish (United States);Spanish (United States)\nes-CU;spa;Spanish (Cuba);Spanish (Cuba)\net;est;Estonian;Estonian\net-EE;est;Estonian (Estonia);Estonian (Estonia)\neu;eus;Basque;Basque\neu-ES;eus;Basque (Spain);Basque (Spain)\nfa;fas;Persian;Persian\nfa-IR;fas;Persian (Iran);Persian (Iran)\nff;ful;Fulah;Fulah\nfi;fin;Finnish;Finnish\nfi-FI;fin;Finnish (Finland);Finnish (Finland)\nfil;fil;Filipino;Filipino\nfil-PH;fil;Filipino (Philippines);Filipino (Philippines)\nfo;fao;Faroese;Faroese\nfo-FO;fao;Faroese (Faroe Islands);Faroese (Faroe Islands)\nfr;fra;French;French\nfr-FR;fra;French (France);French (France)\nfr-BE;fra;French (Belgium);French (Belgium)\nfr-CA;fra;French (Canada);French (Canada)\nfr-CH;fra;French (Switzerland);French (Switzerland)\nfr-LU;fra;French (Luxembourg);French (Luxembourg)\nfr-MC;fra;French (Monaco);French (Monaco)\nfr-RE;fra;French (Réunion);French (Réunion)\nfr-CD;fra;French (Congo - Kinshasa);French (Congo - Kinshasa)\nfr-SN;fra;French (Senegal);French (Senegal)\nfr-CM;fra;French (Cameroon);French (Cameroon)\nfr-CI;fra;French (Côte d’Ivoire);French (Côte d’Ivoire)\nfr-ML;fra;French (Mali);French (Mali)\nfr-MA;fra;French (Morocco);French (Morocco)\nfr-HT;fra;French (Haiti);French (Haiti)\nfy;fry;Western Frisian;Western Frisian\nfy-NL;fry;Western Frisian (Netherlands);Western Frisian (Netherlands)\nga;gle;Irish;Irish\nga-IE;gle;Irish (Ireland);Irish (Ireland)\ngd;gla;Scottish Gaelic;Scottish Gaelic\ngd-GB;gla;Scottish Gaelic (United Kingdom);Scottish Gaelic (United Kingdom)\ngl;glg;Galician;Galician\ngl-ES;glg;Galician (Spain);Galician (Spain)\ngsw;gsw;Swiss German;Swiss German\ngsw-FR;gsw;Swiss German (France);Swiss German (France)\ngu;guj;Gujarati;Gujarati\ngu-IN;guj;Gujarati (India);Gujarati (India)\nha;hau;Hausa;Hausa\nha-Latn-NG;hau;Hausa (Latin, Nigeria);Hausa (Latin, Nigeria)\nha-Latn;hau;Hausa (Latin);Hausa (Latin)\nhaw;haw;Hawaiian;Hawaiian\nhaw-US;haw;Hawaiian (United States);Hawaiian (United States)\nhe;heb;Hebrew;Hebrew\nhe-IL;heb;Hebrew (Israel);Hebrew (Israel)\nhi;hin;Hindi;Hindi\nhi-IN;hin;Hindi (India);Hindi (India)\nhr;hrv;Croatian;Croatian\nhr-HR;hrv;Croatian (Croatia);Croatian (Croatia)\nhr-BA;hrv;Croatian (Bosnia & Herzegovina);Croatian (Bosnia & Herzegovina)\nhsb;hsb;Upper Sorbian;Upper Sorbian\nhsb-DE;hsb;Upper Sorbian (Germany);Upper Sorbian (Germany)\nhu;hun;Hungarian;Hungarian\nhu-HU;hun;Hungarian (Hungary);Hungarian (Hungary)\nhy;hye;Armenian;Armenian\nhy-AM;hye;Armenian (Armenia);Armenian (Armenia)\nid;ind;Indonesian;Indonesian\nid-ID;ind;Indonesian (Indonesia);Indonesian (Indonesia)\nig;ibo;Igbo;Igbo\nig-NG;ibo;Igbo (Nigeria);Igbo (Nigeria)\nii;iii;Sichuan Yi;Sichuan Yi\nii-CN;iii;Sichuan Yi (China);Sichuan Yi (China)\nis;isl;Icelandic;Icelandic\nis-IS;isl;Icelandic (Iceland);Icelandic (Iceland)\nit;ita;Italian;Italian\nit-IT;ita;Italian (Italy);Italian (Italy)\nit-CH;ita;Italian (Switzerland);Italian (Switzerland)\nja;jpn;Japanese;Japanese\nja-JP;jpn;Japanese (Japan);Japanese (Japan)\nka;kat;Georgian;Georgian\nka-GE;kat;Georgian (Georgia);Georgian (Georgia)\nkk;kaz;Kazakh;Kazakh\nkk-KZ;kaz;Kazakh (Kazakhstan);Kazakh (Kazakhstan)\nkl;kal;Kalaallisut;Kalaallisut\nkl-GL;kal;Kalaallisut (Greenland);Kalaallisut (Greenland)\nkm;khm;Khmer;Khmer\nkm-KH;khm;Khmer (Cambodia);Khmer (Cambodia)\nkn;kan;Kannada;Kannada\nkn-IN;kan;Kannada (India);Kannada (India)\nko;kor;Korean;Korean\nko-KR;kor;Korean (South Korea);Korean (South Korea)\nkok;kok;Konkani;Konkani\nkok-IN;kok;Konkani (India);Konkani (India)\nky;kir;Kyrgyz;Kyrgyz\nky-KG;kir;Kyrgyz (Kyrgyzstan);Kyrgyz (Kyrgyzstan)\nlb;ltz;Luxembourgish;Luxembourgish\nlb-LU;ltz;Luxembourgish (Luxembourg);Luxembourgish (Luxembourg)\nlo;lao;Lao;Lao\nlo-LA;lao;Lao (Laos);Lao (Laos)\nlt;lit;Lithuanian;Lithuanian\nlt-LT;lit;Lithuanian (Lithuania);Lithuanian (Lithuania)\nlv;lav;Latvian;Latvian\nlv-LV;lav;Latvian (Latvia);Latvian (Latvia)\nmk;mkd;Macedonian;Macedonian\nmk-MK;mkd;Macedonian (Macedonia);Macedonian (Macedonia)\nml;mal;Malayalam;Malayalam\nml-IN;mal;Malayalam (India);Malayalam (India)\nmn;mon;Mongolian;Mongolian\nmn-MN;mon;Mongolian (Mongolia);Mongolian (Mongolia)\nmn-Cyrl;mon;Mongolian (Cyrillic);Mongolian (Cyrillic)\nmr;mar;Marathi;Marathi\nmr-IN;mar;Marathi (India);Marathi (India)\nms;msa;Malay;Malay\nms-MY;msa;Malay (Malaysia);Malay (Malaysia)\nms-BN;msa;Malay (Brunei);Malay (Brunei)\nmt;mlt;Maltese;Maltese\nmt-MT;mlt;Maltese (Malta);Maltese (Malta)\nmy;mya;Burmese;Burmese\nmy-MM;mya;Burmese (Myanmar (Burma));Burmese (Myanmar (Burma))\nno;nob;Norwegian;Norwegian\nnb-NO;nob;Norwegian Bokmål (Norway);Norwegian Bokmål (Norway)\nnb;nob;Norwegian Bokmål;Norwegian Bokmål\nne;nep;Nepali;Nepali\nne-NP;nep;Nepali (Nepal);Nepali (Nepal)\nne-IN;nep;Nepali (India);Nepali (India)\nnl;nld;Dutch;Dutch\nnl-NL;nld;Dutch (Netherlands);Dutch (Netherlands)\nnl-BE;nld;Dutch (Belgium);Dutch (Belgium)\nnn-NO;nno;Norwegian Nynorsk (Norway);Norwegian Nynorsk (Norway)\nnn;nno;Norwegian Nynorsk;Norwegian Nynorsk\nnso;nso;Northern Sotho;Northern Sotho\nnso-ZA;nso;Northern Sotho (South Africa);Northern Sotho (South Africa)\nom;orm;Oromo;Oromo\nom-ET;orm;Oromo (Ethiopia);Oromo (Ethiopia)\nor;ori;Odia;Odia\nor-IN;ori;Odia (India);Odia (India)\npa;pan;Punjabi;Punjabi\npa-Arab-PK;pan;Punjabi (Arabic, Pakistan);Punjabi (Arabic, Pakistan)\npa-Arab;pan;Punjabi (Arabic);Punjabi (Arabic)\npl;pol;Polish;Polish\npl-PL;pol;Polish (Poland);Polish (Poland)\nps;pus;Pashto;Pashto\nps-AF;pus;Pashto (Afghanistan);Pashto (Afghanistan)\npt;por;Portuguese;Portuguese\npt-BR;por;Portuguese (Brazil);Portuguese (Brazil)\npt-PT;por;Portuguese (Portugal);Portuguese (Portugal)\nrm;roh;Romansh;Romansh\nrm-CH;roh;Romansh (Switzerland);Romansh (Switzerland)\nro;ron;Romanian;Romanian\nro-RO;ron;Romanian (Romania);Romanian (Romania)\nro-MD;ron;Romanian (Moldova);Romanian (Moldova)\nru;rus;Russian;Russian\nru-RU;rus;Russian (Russia);Russian (Russia)\nru-MD;rus;Russian (Moldova);Russian (Moldova)\nrw;kin;Kinyarwanda;Kinyarwanda\nrw-RW;kin;Kinyarwanda (Rwanda);Kinyarwanda (Rwanda)\nsah;sah;Sakha;Sakha\nsah-RU;sah;Sakha (Russia);Sakha (Russia)\nse;sme;Northern Sami;Northern Sami\nse-NO;sme;Northern Sami (Norway);Northern Sami (Norway)\nse-SE;sme;Northern Sami (Sweden);Northern Sami (Sweden)\nse-FI;sme;Northern Sami (Finland);Northern Sami (Finland)\nsi;sin;Sinhala;Sinhala\nsi-LK;sin;Sinhala (Sri Lanka);Sinhala (Sri Lanka)\nsk;slk;Slovak;Slovak\nsk-SK;slk;Slovak (Slovakia);Slovak (Slovakia)\nsl;slv;Slovenian;Slovenian\nsl-SI;slv;Slovenian (Slovenia);Slovenian (Slovenia)\nsmn-FI;smn;Inari Sami (Finland);Inari Sami (Finland)\nsmn;smn;Inari Sami;Inari Sami\nso;som;Somali;Somali\nso-SO;som;Somali (Somalia);Somali (Somalia)\nsq;sqi;Albanian;Albanian\nsq-AL;sqi;Albanian (Albania);Albanian (Albania)\nsr-Latn-BA;srp;Serbian (Latin, Bosnia & Herzegovina);Serbian (Latin, Bosnia & Herzegovina)\nsr-Cyrl-BA;srp;Serbian (Cyrillic, Bosnia & Herzegovina);Serbian (Cyrillic, Bosnia & Herzegovina)\nsr-Latn-RS;srp;Serbian (Latin, Serbia);Serbian (Latin, Serbia)\nsr-Cyrl-RS;srp;Serbian (Cyrillic, Serbia);Serbian (Cyrillic, Serbia)\nsr-Latn-ME;srp;Serbian (Latin, Montenegro);Serbian (Latin, Montenegro)\nsr-Cyrl-ME;srp;Serbian (Cyrillic, Montenegro);Serbian (Cyrillic, Montenegro)\nsr-Cyrl;srp;Serbian (Cyrillic);Serbian (Cyrillic)\nsr-Latn;srp;Serbian (Latin);Serbian (Latin)\nsr;srp;Serbian;Serbian\nst;sot;Southern Sotho;Southern Sotho\nst-ZA;sot;Southern Sotho (South Africa);Southern Sotho (South Africa)\nsv;swe;Swedish;Swedish\nsv-SE;swe;Swedish (Sweden);Swedish (Sweden)\nsv-FI;swe;Swedish (Finland);Swedish (Finland)\nsw;swa;Swahili;Swahili\nsw-KE;swa;Swahili (Kenya);Swahili (Kenya)\nta;tam;Tamil;Tamil\nta-IN;tam;Tamil (India);Tamil (India)\nta-LK;tam;Tamil (Sri Lanka);Tamil (Sri Lanka)\nte;tel;Telugu;Telugu\nte-IN;tel;Telugu (India);Telugu (India)\ntg;tgk;Tajik;Tajik\ntg-Cyrl-TJ;tgk;Tajik (Cyrillic, Tajikistan);Tajik (Cyrillic, Tajikistan)\ntg-Cyrl;tgk;Tajik (Cyrillic);Tajik (Cyrillic)\nth;tha;Thai;Thai\nth-TH;tha;Thai (Thailand);Thai (Thailand)\nti;tir;Tigrinya;Tigrinya\nti-ET;tir;Tigrinya (Ethiopia);Tigrinya (Ethiopia)\nti-ER;tir;Tigrinya (Eritrea);Tigrinya (Eritrea)\ntk;tuk;Turkmen;Turkmen\ntk-TM;tuk;Turkmen (Turkmenistan);Turkmen (Turkmenistan)\ntn;tsn;Tswana;Tswana\ntn-ZA;tsn;Tswana (South Africa);Tswana (South Africa)\ntn-BW;tsn;Tswana (Botswana);Tswana (Botswana)\ntr;tur;Turkish;Turkish\ntr-TR;tur;Turkish (Turkey);Turkish (Turkey)\nts;tso;Tsonga;Tsonga\nts-ZA;tso;Tsonga (South Africa);Tsonga (South Africa)\ntzm;tzm;Central Atlas Tamazight;Central Atlas Tamazight\ntzm-Latn;tzm;Central Atlas Tamazight (Latin);Central Atlas Tamazight (Latin)\nug;uig;Uyghur;Uyghur\nug-CN;uig;Uyghur (China);Uyghur (China)\nuk;ukr;Ukrainian;Ukrainian\nuk-UA;ukr;Ukrainian (Ukraine);Ukrainian (Ukraine)\nur;urd;Urdu;Urdu\nur-PK;urd;Urdu (Pakistan);Urdu (Pakistan)\nur-IN;urd;Urdu (India);Urdu (India)\nuz;uzb;Uzbek;Uzbek\nuz-Latn-UZ;uzb;Uzbek (Latin, Uzbekistan);Uzbek (Latin, Uzbekistan)\nuz-Cyrl-UZ;uzb;Uzbek (Cyrillic, Uzbekistan);Uzbek (Cyrillic, Uzbekistan)\nuz-Cyrl;uzb;Uzbek (Cyrillic);Uzbek (Cyrillic)\nuz-Latn;uzb;Uzbek (Latin);Uzbek (Latin)\nvi;vie;Vietnamese;Vietnamese\nvi-VN;vie;Vietnamese (Vietnam);Vietnamese (Vietnam)\nxh;xho;Xhosa;Xhosa\nxh-ZA;xho;Xhosa (South Africa);Xhosa (South Africa)\nyo;yor;Yoruba;Yoruba\nyo-NG;yor;Yoruba (Nigeria);Yoruba (Nigeria)\nzu;zul;Zulu;Zulu\nzu-ZA;zul;Zulu (South Africa);Zulu (South Africa)\n\n\nzho;chi;中文;中文\nchi;chi;中文;中文\nchs;chi;中文（简体）;中文\nzh-CN;chi;中文（简体）;中文\nzh-SG;chi;中文（简体, 新加坡）;中文\nzh-MO;chi;中文（繁體, 澳門）;中文\nzh-Hans;chi;中文（简体）;中文\nzh-Hant;chi;中文（繁體）;中文\nzh-TW;chi;中文（繁體, 台灣）;中文\nzh-Hant-TW;chi;中文（繁體, 台灣）;中文\nzh-HK;chi;中文（繁體, 香港）;中文\nzh-Hant-HK;chi;中文（繁體, 香港）;中文\nyue;chi;中文（繁體）;粵語\ncmn;chi;中文（简体）;普通话\ncmn-Hans;chi;中文（简体）;普通话\ncmn-Hant;chi;中文（繁體）;普通話\nCantonese;chi;中文;粵語\nMandarin;chi;中文;普通话\nJapanese;jpn;日本語;日本語\nKorean;kor;한국어;한국어\nVietnamese;vie;Vietnamese;Vietnamese\nEnglish;eng;English;English\nThai;tha;Thai;Thai\nCN;chi;中文（繁體）;中文\nCC;chi;中文（繁體）;中文\nCZ;chi;中文（简体）;中文\nMA;msa;Melayu;Melayu\n\"\n        .Trim().Replace(\"\\r\", \"\").Split('\\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x =>\n        {\n            var arr = x.Trim().Split(';', StringSplitOptions.TrimEntries);\n            return new Language(arr[0], arr[1], arr[2], arr[3]);\n        }).ToList();\n\n    private static Dictionary<string, string> CODE_MAP = @\"\niv;IVL\nar;ara\nbg;bul\nca;cat\nzh;zho\ncs;ces\nda;dan\nde;deu\nel;ell\nen;eng\nes;spa\nfi;fin\nfr;fra\nhe;heb\nhu;hun\nis;isl\nit;ita\nja;jpn\nko;kor\nnl;nld\nnb;nob\npl;pol\npt;por\nrm;roh\nro;ron\nru;rus\nhr;hrv\nsk;slk\nsq;sqi\nsv;swe\nth;tha\ntr;tur\nur;urd\nid;ind\nuk;ukr\nbe;bel\nsl;slv\net;est\nlv;lav\nlt;lit\ntg;tgk\nfa;fas\nvi;vie\nhy;hye\naz;aze\neu;eus\nmk;mkd\nst;sot\nts;tso\ntn;tsn\nxh;xho\nzu;zul\naf;afr\nka;kat\nfo;fao\nhi;hin\nmt;mlt\nse;sme\nga;gle\nms;msa\nkk;kaz\nky;kir\nsw;swa\ntk;tuk\nuz;uzb\nbn;ben\npa;pan\ngu;guj\nor;ori\nta;tam\nte;tel\nkn;kan\nml;mal\nas;asm\nmr;mar\nmn;mon\nbo;bod\ncy;cym\nkm;khm\nlo;lao\nmy;mya\ngl;glg\nsi;sin\nam;amh\nne;nep\nfy;fry\nps;pus\nff;ful\nha;hau\nyo;yor\nlb;ltz\nkl;kal\nig;ibo\nom;orm\nti;tir\nso;som\nii;iii\nbr;bre\nug;uig\nrw;kin\ngd;gla\nnn;nno\nbs;bos\nsr;srp\n\"\n        .Trim().Replace(\"\\r\", \"\").Split('\\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).ToDictionary(x => x.Split(';').First().Trim(), x => x.Split(';').Last().Trim());\n\n\n    private static string ConvertTwoToThree(string input)\n    {\n        return CODE_MAP.GetValueOrDefault(input, input);\n    }\n\n    /// <summary>\n    /// 转换 ISO 639-1 => ISO 639-2\n    /// 且当Description为空时将DisplayName写入\n    /// </summary>\n    /// <param name=\"outputFile\"></param>\n    public static void ConvertLangCodeAndDisplayName(OutputFile outputFile)\n    {\n        if (string.IsNullOrEmpty(outputFile.LangCode)) return;\n        var originalLangCode = outputFile.LangCode;\n\n        // 先直接查找\n        var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase));\n        // 处理特殊的扩展语言标记\n        if (lang == null)\n        {\n            // 2位转3位\n            var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First());\n            lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase));\n        }\n\n        if (lang != null)\n        {\n            outputFile.LangCode = lang.Code;\n            if (string.IsNullOrEmpty(outputFile.Description))\n                outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio;\n        }\n        else\n        {\n            outputFile.LangCode = \"und\"; // 无法识别直接置为und\n        }\n\n        // 无描述，则把LangCode当作描述\n        if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/LargeSingleFileSplitUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Util;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static class LargeSingleFileSplitUtil\n{\n    class Clip\n    {\n        public required int Index;\n        public required long From;\n        public required long To;\n    }\n\n    /// <summary>\n    /// URL大文件切片处理\n    /// </summary>\n    /// <param name=\"segment\"></param>\n    /// <param name=\"headers\"></param>\n    /// <returns></returns>\n    public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers)\n    {\n        var url = segment.Url;\n        if (!await CanSplitAsync(url, headers)) return null;\n\n        if (segment.StartRange != null) return null;\n\n        long fileSize = await GetFileSizeAsync(url, headers);\n        if (fileSize == 0) return null;\n\n        List<Clip> allClips = GetAllClips(fileSize);\n        var splitSegments = new List<MediaSegment>();\n        foreach (Clip clip in allClips)\n        {\n            splitSegments.Add(new MediaSegment()\n            {\n                Index = clip.Index,\n                Url = url,\n                StartRange = clip.From,\n                ExpectLength = clip.To == -1 ? null : clip.To - clip.From + 1,\n                EncryptInfo = segment.EncryptInfo,\n            });\n        }\n\n        return splitSegments;\n    }\n\n    public static async Task<bool> CanSplitAsync(string url, Dictionary<string, string> headers)\n    {\n        try\n        {\n            var request = new HttpRequestMessage(HttpMethod.Head, url);\n            var response = (await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();\n            bool supportsRangeRequests = response.Headers.Contains(\"Accept-Ranges\");\n\n            return supportsRangeRequests;\n        }\n        catch (Exception ex)\n        {\n            Logger.DebugMarkUp(ex.Message);\n            return false;\n        }\n    }\n\n    private static async Task<long> GetFileSizeAsync(string url, Dictionary<string, string> headers)\n    {\n        using var httpRequestMessage = new HttpRequestMessage();\n        httpRequestMessage.Method = HttpMethod.Head;\n        httpRequestMessage.RequestUri = new(url);\n        foreach (var header in headers)\n        {\n            httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);\n        }\n        var response = (await HTTPUtil.AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();\n        long totalSizeBytes = response.Content.Headers.ContentLength ?? 0;\n\n        return totalSizeBytes;\n    }\n\n    // 此函数主要是切片下载逻辑\n    private static List<Clip> GetAllClips(long fileSize)\n    {\n        long originalFileSize = fileSize;\n        List<Clip> clips = [];\n        int index = 0;\n        long counter = 0;\n        int perSize = 10 * 1024 * 1024;\n        while (fileSize > 0)\n        {\n            Clip c = new()\n            {\n                Index = index,\n                From = counter,\n                To = counter + perSize\n            };\n            // 没到最后\n            if (fileSize - perSize > 0)\n            {\n                fileSize -= perSize;\n                counter += perSize + 1;\n                index++;\n                clips.Add(c);\n            }\n            // 已到最后\n            else\n            {\n                c.To = originalFileSize;\n                clips.Add(c);\n                break;\n            }\n        }\n        return clips;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/MP4DecryptUtil.cs",
    "content": "﻿using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing System.Diagnostics;\nusing System.Text.RegularExpressions;\nusing N_m3u8DL_RE.Enum;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static partial class MP4DecryptUtil\n{\n    private static readonly string ZeroKid = \"00000000000000000000000000000000\";\n    public static async Task<bool> DecryptAsync(DecryptEngine decryptEngine, string bin, string[]? keys, string source, string dest, string? kid, string init = \"\", bool isMultiDRM=false)\n    {\n        if (keys == null || keys.Length == 0) return false;\n\n        var keyPairs = keys.ToList();\n        string? keyPair = null;\n        string? trackId = null;\n        string? tmpEncFile = null;\n        string? tmpDecFile = null;\n        string? workDir = null;\n\n        if (isMultiDRM)\n        {\n            trackId = \"1\";\n        }\n\n        if (!string.IsNullOrEmpty(kid))\n        {\n            var test = keyPairs.Where(k => k.StartsWith(kid)).ToList();\n            if (test.Count != 0) keyPair = test.First();\n        }\n\n        // Apple\n        if (kid == ZeroKid)\n        {\n            keyPair = keyPairs.First();\n            trackId = \"1\";\n        }\n\n        // user only input key, append kid\n        if (keyPair == null && keyPairs.Count == 1 && !keyPairs.First().Contains(':'))\n        {\n            keyPairs = keyPairs.Select(x => $\"{kid}:{x}\").ToList();\n            keyPair = keyPairs.First();\n        }\n            \n        if (keyPair == null) return false;\n\n        // shakaPackager/ffmpeg 无法单独解密init文件\n        if (source.EndsWith(\"_init.mp4\") && decryptEngine != DecryptEngine.MP4DECRYPT) return false;\n\n        string cmd;\n\n        var tmpFile = \"\";\n        if (decryptEngine == DecryptEngine.SHAKA_PACKAGER)\n        {\n            var enc = source;\n            // shakaPackager 手动构造文件\n            if (init != \"\")\n            {\n                tmpFile = Path.ChangeExtension(source, \".itmp\");\n                MergeUtil.CombineMultipleFilesIntoSingleFile([init, source], tmpFile);\n                enc = tmpFile;\n            }\n\n            cmd = $\"--quiet --enable_raw_key_decryption input=\\\"{enc}\\\",stream=0,output=\\\"{dest}\\\" \" +\n                  $\"--keys {(trackId != null ? $\"label={trackId}:\" : \"\")}key_id={(trackId != null ? ZeroKid : kid)}:key={keyPair.Split(':')[1]}\";\n        }\n        else if (decryptEngine == DecryptEngine.MP4DECRYPT)\n        {\n            if (trackId == null)\n            {\n                cmd = string.Join(\" \", keyPairs.Select(k => $\"--key {k}\"));\n            }\n            else\n            {\n                cmd = string.Join(\" \", keyPairs.Select(k => $\"--key {trackId}:{k.Split(':')[1]}\"));\n            }\n            // 解决mp4decrypt中文问题 切换到源文件所在目录并改名再解密\n            workDir = Path.GetDirectoryName(source)!;\n            tmpEncFile = Path.Combine(workDir, $\"{Guid.NewGuid()}{Path.GetExtension(source)}\");\n            tmpDecFile = Path.Combine(workDir, $\"{Path.GetFileNameWithoutExtension(tmpEncFile)}_dec{Path.GetExtension(tmpEncFile)}\");\n            File.Move(source, tmpEncFile);\n            if (init != \"\")\n            {\n                var infoFile = Path.GetDirectoryName(init) == workDir ? Path.GetFileName(init) : init;\n                cmd += $\" --fragments-info \\\"{infoFile}\\\" \";\n            }\n            cmd += $\" \\\"{Path.GetFileName(tmpEncFile)}\\\" \\\"{Path.GetFileName(tmpDecFile)}\\\"\";\n        }\n        else\n        {\n            var enc = source;\n            // ffmpeg实时解密 手动构造文件\n            if (init != \"\")\n            {\n                tmpFile = Path.ChangeExtension(source, \".itmp\");\n                MergeUtil.CombineMultipleFilesIntoSingleFile([init, source], tmpFile);\n                enc = tmpFile;\n            }\n            \n            cmd = $\"-loglevel error -nostdin -decryption_key {keyPair.Split(':')[1]} -i \\\"{enc}\\\" -c copy \\\"{dest}\\\"\";\n        }\n\n        var isSuccess = await RunCommandAsync(bin, cmd, workDir);\n        \n        // mp4decrypt 还原文件改名操作\n        if (workDir is not null)\n        {\n            if (File.Exists(tmpEncFile)) File.Move(tmpEncFile, source);\n            if (File.Exists(tmpDecFile)) File.Move(tmpDecFile, dest);\n        }\n\n        if (isSuccess)\n        {\n            if (tmpFile != \"\" && File.Exists(tmpFile)) File.Delete(tmpFile);\n            return true;\n        }\n        \n        Logger.Error(ResString.decryptionFailed);\n        return false;\n    }\n\n    private static async Task<bool> RunCommandAsync(string name, string arg, string? workDir = null)\n    {\n        Logger.DebugMarkUp($\"FileName: {name}\");\n        Logger.DebugMarkUp($\"Arguments: {arg}\");\n        var process = Process.Start(new ProcessStartInfo()\n        {\n            FileName = name,\n            Arguments = arg,\n            // RedirectStandardOutput = true,\n            // RedirectStandardError = true,\n            CreateNoWindow = true,\n            UseShellExecute = false,\n            WorkingDirectory = workDir\n        });\n        await process!.WaitForExitAsync();\n        return process.ExitCode == 0;\n    }\n\n    /// <summary>\n    /// 从文本文件中查询KID的KEY\n    /// </summary>\n    /// <param name=\"file\">文本文件</param>\n    /// <param name=\"kid\">目标KID</param>\n    /// <returns></returns>\n    public static async Task<string?> SearchKeyFromFileAsync(string? file, string? kid)\n    {\n        try\n        {\n            if (string.IsNullOrEmpty(file) || !File.Exists(file) || string.IsNullOrEmpty(kid)) \n                return null;\n\n            Logger.InfoMarkUp(ResString.searchKey);\n            using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);\n            using var reader = new StreamReader(stream);\n            while (await reader.ReadLineAsync() is { } line)\n            {\n                if (!line.Trim().StartsWith(kid)) continue;\n                \n                Logger.InfoMarkUp($\"[green]OK[/] [grey]{line.Trim()}[/]\");\n                return line.Trim();\n            }\n        }\n        catch (Exception ex)\n        {\n            Logger.ErrorMarkUp(ex.Message);\n        }\n        return null;\n    }\n\n    public static ParsedMP4Info GetMP4Info(byte[] data)\n    {\n        var info = MP4InitUtil.ReadInit(data);\n        if (info.Scheme != null) Logger.WarnMarkUp($\"[grey]Type: {info.Scheme}[/]\");\n        if (info.PSSH != null) Logger.WarnMarkUp($\"[grey]PSSH(WV): {info.PSSH}[/]\");\n        if (info.KID != null) Logger.WarnMarkUp($\"[grey]KID: {info.KID}[/]\");\n        return info;\n    }\n\n    public static ParsedMP4Info GetMP4Info(string output)\n    {\n        using var fs = File.OpenRead(output);\n        var header = new byte[1 * 1024 * 1024]; // 1MB\n        _ = fs.Read(header);\n        return GetMP4Info(header);\n    }\n\n    public static string? ReadInitShaka(string output, string bin)\n    {\n        Regex shakaKeyIdRegex = KidOutputRegex();\n\n        // TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid)\n        //       - stop process\n        //       - remove {output}.tmp.webm\n        var cmd = $\"--quiet --enable_raw_key_decryption input=\\\"{output}\\\",stream=0,output=\\\"{output}.tmp.webm\\\" \" +\n                  $\"--keys key_id={ZeroKid}:key={ZeroKid}\";\n\n        using var p = new Process();\n        p.StartInfo = new ProcessStartInfo()\n        {\n            FileName = bin,\n            Arguments = cmd,\n            RedirectStandardOutput = true,\n            RedirectStandardError = true,\n            UseShellExecute = false\n        };\n        p.Start();\n        var errorOutput = p.StandardError.ReadToEnd();\n        p.WaitForExit();\n        return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value;\n    }\n\n    [GeneratedRegex(\"Key for key_id=([0-9a-f]+) was not found\")]\n    private static partial Regex KidOutputRegex();\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/MediainfoUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Entity;\nusing System.Diagnostics;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static partial class MediainfoUtil\n{\n    [GeneratedRegex(\"  Stream #.*\")]\n    private static partial Regex TextRegex();\n    [GeneratedRegex(@\"#0:\\d(\\[0x\\w+?\\])\")]\n    private static partial Regex IdRegex();\n    [GeneratedRegex(\": (\\\\w+): (.*)\")]\n    private static partial Regex TypeRegex();\n    [GeneratedRegex(\"(.*?)(,|$)\")]\n    private static partial Regex BaseInfoRegex();\n    [GeneratedRegex(@\" \\/ 0x\\w+\")]\n    private static partial Regex ReplaceRegex();\n    [GeneratedRegex(@\"\\d{2,}x\\d+\")]\n    private static partial Regex ResRegex();\n    [GeneratedRegex(@\"\\d+ kb\\/s\")]\n    private static partial Regex BitrateRegex();\n    [GeneratedRegex(@\"(\\d+(\\.\\d+)?) fps\")]\n    private static partial Regex FpsRegex();\n    [GeneratedRegex(@\"DOVI configuration record.*profile: (\\d).*compatibility id: (\\d)\")]\n    private static partial Regex DoViRegex();\n    [GeneratedRegex(@\"Duration.*?start: (\\d+\\.?\\d{0,3})\")]\n    private static partial Regex StartRegex();\n\n    public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file)\n    {\n        var result = new List<Mediainfo>();\n\n        if (string.IsNullOrEmpty(file) || !File.Exists(file)) return result;\n\n        string cmd = \"-hide_banner -i \\\"\" + file + \"\\\"\";\n        var p = Process.Start(new ProcessStartInfo()\n        {\n            FileName = binary,\n            Arguments = cmd,\n            RedirectStandardOutput = true,\n            RedirectStandardError = true,\n            UseShellExecute = false\n        })!;\n        var output = await p.StandardError.ReadToEndAsync();\n        await p.WaitForExitAsync();\n\n        foreach (Match stream in TextRegex().Matches(output))\n        {\n            var info = new Mediainfo()\n            {\n                Text = TypeRegex().Match(stream.Value).Groups[2].Value.TrimEnd(),\n                Id = IdRegex().Match(stream.Value).Groups[1].Value,\n                Type = TypeRegex().Match(stream.Value).Groups[1].Value,\n            };\n\n            info.Resolution = ResRegex().Match(info.Text).Value;\n            info.Bitrate = BitrateRegex().Match(info.Text).Value;\n            info.Fps = FpsRegex().Match(info.Text).Value;\n            info.BaseInfo = BaseInfoRegex().Match(info.Text).Groups[1].Value;\n            info.BaseInfo = ReplaceRegex().Replace(info.BaseInfo, \"\");\n            info.HDR = info.Text.Contains(\"/bt2020/\");\n\n            if (info.BaseInfo.Contains(\"dvhe\")\n                || info.BaseInfo.Contains(\"dvh1\")\n                || info.BaseInfo.Contains(\"DOVI\")\n                || info.Type.Contains(\"dvvideo\")\n                || (DoViRegex().IsMatch(output) && info.Type == \"Video\")\n               )\n                info.DolbyVison = true;\n\n            if (StartRegex().IsMatch(output))\n            {\n                var f = StartRegex().Match(output).Groups[1].Value;\n                if (double.TryParse(f, out var d))\n                    info.StartTime = TimeSpan.FromSeconds(d);\n            }\n\n            result.Add(info);\n        }\n\n        if (result.Count == 0)\n        {\n            result.Add(new Mediainfo\n            {\n                Type = \"Unknown\"\n            });\n        }\n\n        return result;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/MergeUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Entity;\nusing Spectre.Console;\nusing System.Diagnostics;\nusing System.Text;\nusing N_m3u8DL_RE.Enum;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static class MergeUtil\n{\n    /// <summary>\n    /// 输入一堆已存在的文件，合并到新文件\n    /// </summary>\n    /// <param name=\"files\"></param>\n    /// <param name=\"outputFilePath\"></param>\n    public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath)\n    {\n        if (files.Length == 0) return;\n        if (files.Length == 1)\n        {\n            FileInfo fi = new FileInfo(files[0]);\n            fi.CopyTo(outputFilePath, true);\n            return;\n        }\n\n        if (!Directory.Exists(Path.GetDirectoryName(outputFilePath)))\n            Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!);\n\n        var inputFilePaths = files;\n        using var outputStream = File.Create(outputFilePath);\n        foreach (var inputFilePath in inputFilePaths)\n        {\n            if (inputFilePath == \"\")\n                continue;\n            using var inputStream = File.OpenRead(inputFilePath);\n            inputStream.CopyTo(outputStream);\n        }\n    }\n\n    private static int InvokeFFmpeg(string binary, string command, string workingDirectory)\n    {\n        Logger.DebugMarkUp($\"{binary}: {command}\");\n\n        using var p = new Process();\n        p.StartInfo = new ProcessStartInfo()\n        {\n            WorkingDirectory = workingDirectory,\n            FileName = binary,\n            Arguments = command,\n            RedirectStandardOutput = true,\n            RedirectStandardError = true,\n            UseShellExecute = false\n        };\n        p.ErrorDataReceived += (sendProcess, output) =>\n        {\n            if (!string.IsNullOrEmpty(output.Data))\n            {\n                Logger.WarnMarkUp($\"[grey]{output.Data.EscapeMarkup()}[/]\");\n            }\n        };\n        p.Start();\n        p.BeginErrorReadLine();\n        p.WaitForExit();\n        return p.ExitCode;\n    }\n\n    public static string[] PartialCombineMultipleFiles(string[] files)\n    {\n        var newFiles = new List<string>();\n        var div = files.Length <= 90000 ? 100 : 200;\n\n        var outputName = Path.Combine(Path.GetDirectoryName(files[0])!, \"T\");\n        var index = 0; // 序号\n\n        // 按照div的容量分割为小数组\n        var li = Enumerable.Range(0, files.Length / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray();\n        foreach (var items in li)\n        {\n            if (items.Length == 0)\n                continue;\n            var output = outputName + index.ToString(\"0000\") + \".ts\";\n            CombineMultipleFilesIntoSingleFile(items, output);\n            newFiles.Add(output);\n            // 合并后删除这些文件\n            foreach (var item in items)\n            {\n                File.Delete(item);\n            }\n            index++;\n        }\n\n        return newFiles.ToArray();\n    }\n\n    public static bool MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, bool useAACFilter,\n        bool fastStart = false,\n        bool writeDate = true, bool useConcatDemuxer = false, string poster = \"\", string audioName = \"\", string title = \"\",\n        string copyright = \"\", string comment = \"\", string encodingTool = \"\", string recTime = \"\")\n    {\n        // 改为绝对路径\n        outputPath = Path.GetFullPath(outputPath);\n\n        string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString(\"o\") : recTime;\n\n        StringBuilder command = new StringBuilder(\"-loglevel warning -nostdin \");\n        string ddpAudio = string.Empty;\n        string addPoster = \"-map 1 -c:v:1 copy -disposition:v:1 attached_pic\";\n        ddpAudio = (File.Exists($\"{Path.GetFileNameWithoutExtension(outputPath + \".mp4\")}.txt\") ? File.ReadAllText($\"{Path.GetFileNameWithoutExtension(outputPath + \".mp4\")}.txt\") : \"\");\n        if (!string.IsNullOrEmpty(ddpAudio)) useAACFilter = false;\n\n        if (useConcatDemuxer)\n        {\n            // 使用 concat demuxer合并\n            var text = string.Join(Environment.NewLine, files.Select(f => $\"file '{f}'\"));\n            var tempFile = Path.GetTempFileName();\n            File.WriteAllText(tempFile, text);\n            command.Append($\" -f concat -safe 0 -i \\\"{tempFile}\");\n        }\n        else\n        {\n            command.Append(\" -i concat:\\\"\");\n            foreach (string t in files)\n            {\n                command.Append(Path.GetFileName(t) + \"|\");\n            }\n        }\n\n\n        switch (muxFormat.ToUpper())\n        {\n            case (\"MP4\"):\n                command.Append(\"\\\" \" + (string.IsNullOrEmpty(poster) ? \"\" : \"-i \\\"\" + poster + \"\\\"\"));\n                command.Append(\" \" + (string.IsNullOrEmpty(ddpAudio) ? \"\" : \"-i \\\"\" + ddpAudio + \"\\\"\"));\n                command.Append(\n                    $\" -map 0:v? {(string.IsNullOrEmpty(ddpAudio) ? \"-map 0:a?\" : $\"-map {(string.IsNullOrEmpty(poster) ? \"1\" : \"2\")}:a -map 0:a?\")} -map 0:s? \" + (string.IsNullOrEmpty(poster) ? \"\" : addPoster)\n                    + (writeDate ? \" -metadata date=\\\"\" + dateString + \"\\\"\" : \"\") +\n                    \" -metadata encoding_tool=\\\"\" + encodingTool + \"\\\" -metadata title=\\\"\" + title +\n                    \"\\\" -metadata copyright=\\\"\" + copyright + \"\\\" -metadata comment=\\\"\" + comment +\n                    $\"\\\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? \"0\" : \"1\")} title=\\\"\" + audioName + $\"\\\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? \"0\" : \"1\")} handler=\\\"\" + audioName + \"\\\" \");\n                command.Append(string.IsNullOrEmpty(ddpAudio) ? \"\" : \" -metadata:s:a:0 title=\\\"DD+\\\" -metadata:s:a:0 handler=\\\"DD+\\\" \");\n                if (fastStart)\n                    command.Append(\"-movflags +faststart\");\n                command.Append(\"  -c copy -y \" + (useAACFilter ? \"-bsf:a aac_adtstoasc\" : \"\") + \" \\\"\" + outputPath + \".mp4\\\"\");\n                break;\n            case (\"MKV\"):\n                command.Append(\"\\\" -map 0  -c copy -y \" + (useAACFilter ? \"-bsf:a aac_adtstoasc\" : \"\") + \" \\\"\" + outputPath + \".mkv\\\"\");\n                break;\n            case (\"FLV\"):\n                command.Append(\"\\\" -map 0  -c copy -y \" + (useAACFilter ? \"-bsf:a aac_adtstoasc\" : \"\") + \" \\\"\" + outputPath + \".flv\\\"\");\n                break;\n            case (\"M4A\"):\n                command.Append(\"\\\" -map 0  -c copy -f mp4 -y \" + (useAACFilter ? \"-bsf:a aac_adtstoasc\" : \"\") + \" \\\"\" + outputPath + \".m4a\\\"\");\n                break;\n            case (\"TS\"):\n                command.Append(\"\\\" -map 0  -c copy -y -f mpegts -bsf:v h264_mp4toannexb \\\"\" + outputPath + \".ts\\\"\");\n                break;\n            case (\"EAC3\"):\n                command.Append(\"\\\" -map 0:a -c copy -y \\\"\" + outputPath + \".eac3\\\"\");\n                break;\n            case (\"AAC\"):\n                command.Append(\"\\\" -map 0:a -c copy -y \\\"\" + outputPath + \".m4a\\\"\");\n                break;\n            case (\"AC3\"):\n                command.Append(\"\\\" -map 0:a -c copy -y \\\"\" + outputPath + \".ac3\\\"\");\n                break;\n        }\n\n        var code = InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0])!);\n\n        return code == 0;\n    }\n\n    public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath, MuxFormat muxFormat, bool dateinfo)\n    {\n        var ext = OtherUtil.GetMuxExtension(muxFormat);\n        string dateString = DateTime.Now.ToString(\"o\");\n        StringBuilder command = new StringBuilder(\"-loglevel warning -nostdin -y -dn \");\n\n        // INPUT\n        foreach (var item in files)\n        {\n            command.Append($\" -i \\\"{item.FilePath}\\\" \");\n        }\n\n        // MAP\n        for (int i = 0; i < files.Length; i++)\n        {\n            command.Append($\" -map {i} \");\n        }\n\n        var srt = files.Any(x => x.FilePath.EndsWith(\".srt\"));\n\n        if (muxFormat == MuxFormat.MP4)\n            command.Append($\" -strict unofficial -c:a copy -c:v copy -c:s mov_text \"); // mp4不支持vtt/srt字幕，必须转换格式\n        else if (muxFormat == MuxFormat.TS)\n            command.Append($\" -strict unofficial -c:a copy -c:v copy \");\n        else if (muxFormat == MuxFormat.MKV)\n            command.Append($\" -strict unofficial -c:a copy -c:v copy -c:s {(srt ? \"srt\" : \"webvtt\")} \");\n        else throw new ArgumentException($\"unknown format: {muxFormat}\");\n\n        // CLEAN\n        command.Append(\" -map_metadata -1 \");\n\n        // LANG and NAME\n        var streamIndex = 0;\n        for (int i = 0; i < files.Length; i++)\n        {\n            // 转换语言代码\n            LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);\n            command.Append($\" -metadata:s:{streamIndex} language=\\\"{files[i].LangCode ?? \"und\"}\\\" \");\n            if (!string.IsNullOrEmpty(files[i].Description))\n            {\n                command.Append($\" -metadata:s:{streamIndex} title=\\\"{files[i].Description}\\\" \");\n            }\n            /**\n             * -metadata:s:xx标记的是 输出的第xx个流的metadata，\n             * 若输入文件存在不止一个流时，这里单纯使用files的index\n             * 就有可能出现metadata错位的情况，所以加了如下逻辑\n             */\n            if (files[i].Mediainfos.Count > 0)\n                streamIndex += files[i].Mediainfos.Count;\n            else\n                streamIndex++;\n        }\n\n        var videoTracks = files.Where(x => x.MediaType != Common.Enum.MediaType.AUDIO && x.MediaType != Common.Enum.MediaType.SUBTITLES);\n        var audioTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO);\n        var subTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO);\n        if (videoTracks.Any()) command.Append(\" -disposition:v:0 default \");\n        // 字幕都不设置默认\n        if (subTracks.Any()) command.Append(\" -disposition:s 0 \");\n        if (audioTracks.Any())\n        {\n            // 音频除了第一个音轨 都不设置默认\n            command.Append(\" -disposition:a:0 default \");\n            for (int i = 1; i < audioTracks.Count(); i++)\n            {\n                command.Append($\" -disposition:a:{i} 0 \");\n            }\n        }\n\n        if (dateinfo) command.Append($\" -metadata date=\\\"{dateString}\\\" \");\n        command.Append($\" -ignore_unknown -copy_unknown \");\n        command.Append($\" \\\"{outputPath}{ext}\\\"\");\n\n        var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory);\n\n        return code == 0;\n    }\n\n    public static bool MuxInputsByMkvmerge(string binary, OutputFile[] files, string outputPath)\n    {\n        StringBuilder command = new StringBuilder($\"-q --output \\\"{outputPath}.mkv\\\" \");\n\n        command.Append(\" --no-chapters \");\n\n        var dFlag = false;\n\n        // LANG and NAME\n        for (int i = 0; i < files.Length; i++)\n        {\n            // 转换语言代码\n            LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);\n            command.Append($\" --language 0:\\\"{files[i].LangCode ?? \"und\"}\\\" \");\n            // 字幕都不设置默认\n            if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES)\n                command.Append($\" --default-track 0:no \");\n            // 音频除了第一个音轨 都不设置默认\n            if (files[i].MediaType == Common.Enum.MediaType.AUDIO)\n            {\n                if (dFlag)\n                    command.Append($\" --default-track 0:no \");\n                dFlag = true;\n            }\n            if (!string.IsNullOrEmpty(files[i].Description))\n                command.Append($\" --track-name 0:\\\"{files[i].Description}\\\" \");\n            command.Append($\" \\\"{files[i].FilePath}\\\" \");\n        }\n\n        var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory);\n\n        return code == 0;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/OtherUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Enum;\nusing System.IO.Compression;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static partial class OtherUtil\n{\n    public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers)\n    {\n        Dictionary<string, string> dic = new();\n        if (headers == null) return dic;\n        \n        foreach (string header in headers)\n        {\n            var index = header.IndexOf(':');\n            if (index != -1)\n            {\n                dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim();\n            }\n        }\n\n        return dic;\n    }\n\n    private static readonly char[] InvalidChars = \"34,60,62,124,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,58,42,63,92,47\"\n        .Split(',').Select(s => (char)int.Parse(s)).ToArray();\n    public static string GetValidFileName(string input, string re = \"_\", bool filterSlash = false)\n    {\n        var title = InvalidChars.Aggregate(input, (current, invalidChar) => current.Replace(invalidChar.ToString(), re));\n        if (filterSlash)\n        {\n            title = title.Replace(\"/\", re);\n            title = title.Replace(\"\\\\\", re);\n        }\n        return title.Trim('.');\n    }\n\n    /// <summary>\n    /// 从输入自动获取文件名\n    /// </summary>\n    /// <param name=\"input\"></param>\n    /// <param name=\"addSuffix\"></param>\n    /// <returns></returns>\n    public static string GetFileNameFromInput(string input, bool addSuffix = true)\n    {\n        var saveName = addSuffix ? DateTime.Now.ToString(\"yyyy-MM-dd_HH-mm-ss\") : string.Empty;\n        if (File.Exists(input))\n        {\n            saveName = Path.GetFileNameWithoutExtension(input) + \"_\" + saveName;\n        }\n        else\n        {\n            var uri = new Uri(input.Split('?').First());\n            var name = Path.GetFileNameWithoutExtension(uri.LocalPath);\n            saveName = GetValidFileName(name) + \"_\" + saveName;\n        }\n        return saveName;\n    }\n\n    /// <summary>\n    /// 从 hh:mm:ss 解析TimeSpan\n    /// </summary>\n    /// <param name=\"timeStr\"></param>\n    /// <returns></returns>\n    public static TimeSpan ParseDur(string timeStr)\n    {\n        var arr = timeStr.Replace(\"：\", \":\").Split(':');\n        var days = -1;\n        var hours = -1;\n        var mins = -1;\n        var secs = -1;\n        arr.Reverse().Select(i => Convert.ToInt32(i)).ToList().ForEach(item =>\n        {\n            if (secs == -1) secs = item;\n            else if (mins == -1) mins = item;\n            else if (hours == -1) hours = item;\n            else if (days == -1) days = item;\n        });\n\n        if (days == -1) days = 0;\n        if (hours == -1) hours = 0;\n        if (mins == -1) mins = 0;\n        if (secs == -1) secs = 0;\n\n        return new TimeSpan(days, hours, mins, secs);\n    }\n\n    /// <summary>\n    /// 从1h3m20s解析出总秒数\n    /// </summary>\n    /// <param name=\"timeStr\"></param>\n    /// <returns></returns>\n    /// <exception cref=\"ArgumentException\"></exception>\n    public static double ParseSeconds(string timeStr)\n    {\n        var pattern = TimeStrRegex();\n\n        var match = pattern.Match(timeStr);\n\n        if (!match.Success)\n        {\n            throw new ArgumentException(\"时间格式无效\");\n        }\n\n        int hours = match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0;\n        int minutes = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0;\n        int seconds = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0;\n\n        return hours * 3600 + minutes * 60 + seconds;\n    }\n\n    // 若该文件夹为空，删除，同时判断其父文件夹，直到遇到根目录或不为空的目录\n    public static void SafeDeleteDir(string dirPath)\n    {\n        if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath))\n            return;\n\n        var parent = Path.GetDirectoryName(dirPath)!;\n        if (!Directory.EnumerateFileSystemEntries(dirPath).Any())\n        {\n            Directory.Delete(dirPath);\n        }\n        else\n        {\n            return;\n        }\n        SafeDeleteDir(parent);\n    }\n\n    /// <summary>\n    /// 解压并替换原文件\n    /// </summary>\n    /// <param name=\"filePath\"></param>\n    public static async Task DeGzipFileAsync(string filePath)\n    {\n        var deGzipFile = Path.ChangeExtension(filePath, \".dezip_tmp\");\n        try\n        {\n            await using (var fileToDecompressAsStream = File.OpenRead(filePath))\n            {\n                await using var decompressedStream = File.Create(deGzipFile);\n                await using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress);\n                await decompressionStream.CopyToAsync(decompressedStream);\n            };\n            File.Delete(filePath);\n            File.Move(deGzipFile, filePath);\n        }\n        catch \n        {\n            if (File.Exists(deGzipFile)) File.Delete(deGzipFile);\n        }\n    }\n\n    public static string GetEnvironmentVariable(string key, string defaultValue = \"\")\n    {\n        return Environment.GetEnvironmentVariable(key) ?? defaultValue;\n    }\n\n    public static string GetMuxExtension(MuxFormat muxFormat)\n    {\n        return muxFormat switch\n        {\n            MuxFormat.MP4 => \".mp4\",\n            MuxFormat.MKV => \".mkv\",\n            MuxFormat.TS => \".ts\",\n            _ => throw new ArgumentException($\"unknown format: {muxFormat}\")\n        };\n    }\n\n    [GeneratedRegex(@\"^(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?$\")]\n    private static partial Regex TimeStrRegex();\n\n    /// <summary>\n    /// 格式化保存模板\n    /// </summary>\n    /// <param name=\"savePattern\">模板字符串</param>\n    /// <param name=\"streamSpec\">流规格</param>\n    /// <param name=\"saveName\">保存名称</param>\n    /// <param name=\"taskId\">任务ID</param>\n    /// <returns>格式化后的文件名(不含扩展名)</returns>\n    public static string FormatSavePattern(string savePattern, Common.Entity.StreamSpec streamSpec, string? saveName, int taskId)\n    {\n        var result = savePattern;\n\n        // 替换基本变量\n        result = result.Replace(\"<SaveName>\", saveName ?? \"\");\n        result = result.Replace(\"<Id>\", taskId.ToString());\n        result = result.Replace(\"<Codecs>\", streamSpec.Codecs ?? \"\");\n        result = result.Replace(\"<Language>\", streamSpec.Language ?? \"\");\n        result = result.Replace(\"<Bandwidth>\", streamSpec.Bandwidth?.ToString() ?? \"\");\n        result = result.Replace(\"<Resolution>\", streamSpec.Resolution ?? \"\");\n        result = result.Replace(\"<FrameRate>\", streamSpec.FrameRate?.ToString() ?? \"\");\n        result = result.Replace(\"<Channels>\", streamSpec.Channels ?? \"\");\n        result = result.Replace(\"<VideoRange>\", streamSpec.VideoRange ?? \"\");\n        result = result.Replace(\"<MediaType>\", streamSpec.MediaType?.ToString() ?? \"\");\n        result = result.Replace(\"<GroupId>\", streamSpec.GroupId ?? \"\");\n\n        // 清理多余的分隔符\n        result = result.Replace(\"__\", \"_\").Replace(\"..\", \".\").Trim('_').Trim('.');\n\n        // 清理文件名中的非法字符\n        return GetValidFileName(result);\n    }\n\n    /// <summary>\n    /// 处理文件名冲突，使用流元数据生成唯一文件名\n    /// </summary>\n    /// <param name=\"originalPath\">原始文件路径</param>\n    /// <param name=\"streamSpec\">流规格（用于获取元数据）</param>\n    /// <returns>不冲突的文件路径</returns>\n    public static string HandleFileCollision(string originalPath, Common.Entity.StreamSpec streamSpec)\n    {\n        if (!File.Exists(originalPath))\n            return originalPath;\n\n        var dir = Path.GetDirectoryName(originalPath) ?? \"\";\n        var nameWithoutExt = Path.GetFileNameWithoutExtension(originalPath);\n        var ext = Path.GetExtension(originalPath);\n\n        // 尝试使用元数据生成唯一文件名\n        var attempts = new List<string>();\n\n        // 对于视频流，尝试添加分辨率和带宽\n        if (streamSpec.MediaType == Common.Enum.MediaType.VIDEO)\n        {\n            if (!string.IsNullOrEmpty(streamSpec.Resolution))\n            {\n                attempts.Add($\"{nameWithoutExt}.{streamSpec.Resolution}{ext}\");\n            }\n            if (streamSpec.Bandwidth.HasValue)\n            {\n                var bandwidthMbps = streamSpec.Bandwidth.Value / 1000000.0;\n                attempts.Add($\"{nameWithoutExt}.{bandwidthMbps:F1}Mbps{ext}\");\n            }\n            if (!string.IsNullOrEmpty(streamSpec.Resolution) && streamSpec.Bandwidth.HasValue)\n            {\n                var bandwidthMbps = streamSpec.Bandwidth.Value / 1000000.0;\n                attempts.Add($\"{nameWithoutExt}.{streamSpec.Resolution}.{bandwidthMbps:F1}Mbps{ext}\");\n            }\n        }\n        // 对于音频流，尝试添加语言、声道和带宽\n        else if (streamSpec.MediaType == Common.Enum.MediaType.AUDIO)\n        {\n            if (!string.IsNullOrEmpty(streamSpec.Language))\n            {\n                attempts.Add($\"{nameWithoutExt}.{streamSpec.Language}{ext}\");\n            }\n            if (!string.IsNullOrEmpty(streamSpec.Channels))\n            {\n                attempts.Add($\"{nameWithoutExt}.{streamSpec.Channels}ch{ext}\");\n            }\n            if (!string.IsNullOrEmpty(streamSpec.Language) && !string.IsNullOrEmpty(streamSpec.Channels))\n            {\n                attempts.Add($\"{nameWithoutExt}.{streamSpec.Language}.{streamSpec.Channels}ch{ext}\");\n            }\n            if (streamSpec.Bandwidth.HasValue)\n            {\n                var bandwidthKbps = streamSpec.Bandwidth.Value / 1000;\n                attempts.Add($\"{nameWithoutExt}.{bandwidthKbps}kbps{ext}\");\n            }\n        }\n        // 对于字幕流，尝试添加语言\n        else if (streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES)\n        {\n            if (!string.IsNullOrEmpty(streamSpec.Language))\n            {\n                attempts.Add($\"{nameWithoutExt}.{streamSpec.Language}{ext}\");\n            }\n        }\n\n        // 尝试所有基于元数据的文件名\n        foreach (var attempt in attempts)\n        {\n            var attemptPath = Path.Combine(dir, attempt);\n            if (!File.Exists(attemptPath))\n                return attemptPath;\n        }\n\n        // 所有元数据方案都失败，回退到 \"copy\" 方案\n        var output = originalPath;\n        while (File.Exists(output))\n        {\n            output = Path.Combine(dir, $\"{Path.GetFileNameWithoutExtension(output)}.copy{Path.GetExtension(output)}\");\n        }\n        return output;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/PipeUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Log;\nusing Spectre.Console;\nusing System.Diagnostics;\nusing System.IO.Pipes;\nusing System.Text;\nusing N_m3u8DL_RE.Config;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static class PipeUtil\n{\n    public static Stream CreatePipe(string pipeName)\n    {\n        if (OperatingSystem.IsWindows())\n        {\n            return new NamedPipeServerStream(pipeName, PipeDirection.InOut);\n        }\n\n        var path = Path.Combine(Path.GetTempPath(), pipeName);\n        using var p = new Process();\n        p.StartInfo = new ProcessStartInfo()\n        {\n            FileName = \"mkfifo\",\n            Arguments = path,\n            CreateNoWindow = true,\n            UseShellExecute = false,\n            RedirectStandardError = true,\n            RedirectStandardOutput = true,\n        };\n        p.Start();\n        p.WaitForExit();\n        Thread.Sleep(200);\n        return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);\n    }\n\n    public static async Task<bool> StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath)\n    {\n        return await Task.Run(async () =>\n        {\n            await Task.Delay(1000);\n            return StartPipeMux(binary, pipeNames, outputPath);\n        });\n    }\n\n    public static bool StartPipeMux(string binary, string[] pipeNames, string outputPath)\n    {\n        var dateString = DateTime.Now.ToString(\"o\");\n        var command = new StringBuilder(\"-y -fflags +genpts -loglevel quiet \");\n\n        var customDest = OtherUtil.GetEnvironmentVariable(EnvConfigKey.ReLivePipeOptions);\n        var pipeDir = OtherUtil.GetEnvironmentVariable(EnvConfigKey.ReLivePipeTmpDir, Path.GetTempPath());\n\n        if (!string.IsNullOrEmpty(customDest))\n        {\n            command.Append(\" -re \");\n        }\n\n        foreach (var item in pipeNames)\n        {\n            if (OperatingSystem.IsWindows())\n                command.Append($\" -i \\\"\\\\\\\\.\\\\pipe\\\\{item}\\\" \");\n            else\n                // command.Append($\" -i \\\"unix://{Path.Combine(Path.GetTempPath(), $\"CoreFxPipe_{item}\")}\\\" \");\n                command.Append($\" -i \\\"{Path.Combine(pipeDir, item)}\\\" \");\n        }\n\n        for (var i = 0; i < pipeNames.Length; i++)\n        {\n            command.Append($\" -map {i} \");\n        }\n\n        command.Append(\" -strict unofficial -c copy \");\n        command.Append($\" -metadata date=\\\"{dateString}\\\" \");\n        command.Append($\" -ignore_unknown -copy_unknown \");\n\n\n        if (!string.IsNullOrEmpty(customDest))\n        {\n            if (customDest.Trim().StartsWith('-'))\n                command.Append(customDest);\n            else\n                command.Append($\" -f mpegts -shortest \\\"{customDest}\\\"\");\n            Logger.WarnMarkUp($\"[deepskyblue1]{command.ToString().EscapeMarkup()}[/]\");\n        }\n        else\n        {\n            command.Append($\" -f mpegts -shortest \\\"{outputPath}\\\"\");\n        }\n\n        using var p = new Process();\n        p.StartInfo = new ProcessStartInfo()\n        {\n            WorkingDirectory = Environment.CurrentDirectory,\n            FileName = binary,\n            Arguments = command.ToString(),\n            CreateNoWindow = true,\n            UseShellExecute = false\n        };\n        // p.StartInfo.Environment.Add(\"FFREPORT\", \"file=ffreport.log:level=42\");\n        p.Start();\n        p.WaitForExit();\n\n        return p.ExitCode == 0;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE/Util/SubtitleUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\n\nnamespace N_m3u8DL_RE.Util;\n\ninternal static class SubtitleUtil\n{\n    /// <summary>\n    /// 写出图形字幕PNG文件\n    /// </summary>\n    /// <param name=\"finalVtt\"></param>\n    /// <param name=\"tmpDir\">临时目录</param>\n    /// <returns></returns>\n    public static async Task TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir)\n    {\n        if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith(\"Base64::\")))\n        {\n            Logger.WarnMarkUp(ResString.processImageSub);\n            var i = 0;\n            foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith(\"Base64::\")))\n            {\n                var name = $\"{i++}.png\";\n                var dest = \"\";\n                for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $\"{i++}.png\") ;\n                var base64 = img.Payload[8..];\n                await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64));\n                img.Payload = name;\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/EncryptInfo.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Common.Entity;\n\npublic class EncryptInfo\n{\n    /// <summary>\n    /// 加密方式，默认无加密\n    /// </summary>\n    public EncryptMethod Method { get; set; } = EncryptMethod.NONE;\n\n    public byte[]? Key { get; set; }\n    public byte[]? IV { get; set; }\n\n    public EncryptInfo() { }\n\n    /// <summary>\n    /// 创建EncryptInfo并尝试自动解析Method\n    /// </summary>\n    /// <param name=\"method\"></param>\n    public EncryptInfo(string method)\n    {\n        Method = ParseMethod(method);\n    }\n\n    public static EncryptMethod ParseMethod(string? method)\n    {\n        if (method != null && System.Enum.TryParse(method.Replace(\"-\", \"_\"), out EncryptMethod m))\n        {\n            return m;\n        }\n        return EncryptMethod.UNKNOWN;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/MSSData.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Entity;\n\npublic class MSSData\n{\n    public string FourCC { get; set; } = \"\";\n    public string CodecPrivateData { get; set; } = \"\";\n    public string Type { get; set; } = \"\";\n    public int Timesacle { get; set; }\n    public int SamplingRate { get; set; }\n    public int Channels { get; set; }\n    public int BitsPerSample { get; set; }\n    public int NalUnitLengthField { get; set; }\n    public long Duration { get; set; }\n\n    public bool IsProtection { get; set; } = false;\n    public string ProtectionSystemID { get; set; } = \"\";\n    public string ProtectionData { get; set; } = \"\";\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/MediaPart.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Entity;\n\n// 主要处理 EXT-X-DISCONTINUITY\npublic class MediaPart\n{\n    public List<MediaSegment> MediaSegments { get; set; } = [];\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/MediaSegment.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Common.Entity;\n\npublic class MediaSegment\n{\n    public long Index { get; set; }\n    public double Duration { get; set; }\n    public string? Title { get; set; }\n    public DateTime? DateTime { get; set; }\n\n    public long? StartRange { get; set; }\n    public long? StopRange => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null;\n    public long? ExpectLength { get; set; }\n\n    public EncryptInfo EncryptInfo { get; set; } = new();\n    \n    public bool IsEncrypted => EncryptInfo.Method != EncryptMethod.NONE;\n\n    public string Url { get; set; } = string.Empty;\n\n    public string? NameFromVar { get; set; } // MPD分段文件名\n\n    public override bool Equals(object? obj)\n    {\n        return obj is MediaSegment segment &&\n               Index == segment.Index &&\n               Math.Abs(Duration - segment.Duration) < 0.001 &&\n               Title == segment.Title &&\n               StartRange == segment.StartRange &&\n               StopRange == segment.StopRange &&\n               ExpectLength == segment.ExpectLength &&\n               Url == segment.Url;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/Playlist.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Entity;\n\npublic class Playlist\n{\n    // 对应Url信息\n    public string Url { get; set; } = string.Empty;\n    // 是否直播\n    public bool IsLive { get; set; } = false;\n    // 直播刷新间隔毫秒（默认15秒）\n    public double RefreshIntervalMs { get; set; } = 15000;\n    // 所有分片时长总和\n    public double TotalDuration => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration));\n\n    // 所有分片中最长时长\n    public double? TargetDuration { get; set; }\n    // INIT信息\n    public MediaSegment? MediaInit { get; set; }\n    // 分片信息\n    public List<MediaPart> MediaParts { get; set; } = [];\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/StreamSpec.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Util;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Common.Entity;\n\npublic class StreamSpec\n{\n    public MediaType? MediaType { get; set; }\n    public string? GroupId { get; set; }\n    public string? Language { get; set; }\n    public string? Name { get; set; }\n    public Choise? Default { get; set; }\n\n    // 由于用户选择 被跳过的分片总时长\n    public double? SkippedDuration { get; set; }\n\n    // MSS信息\n    public MSSData? MSSData { get; set; }\n\n    // 基本信息\n    public int? Bandwidth { get; set; }\n    public string? Codecs { get; set; }\n    public string? Resolution { get; set; }\n    public double? FrameRate { get; set; }\n    public string? Channels { get; set; }\n    public string? Extension { get; set; }\n\n    // Dash\n    public RoleType? Role { get; set; }\n\n    // 补充信息-色域\n    public string? VideoRange { get; set; }\n    // 补充信息-特征\n    public string? Characteristics { get; set; }\n    // 发布时间（仅MPD需要）\n    public DateTime? PublishTime { get; set; }\n\n    // 外部轨道GroupId (后续寻找对应轨道信息)\n    public string? AudioId { get; set; }\n    public string? VideoId { get; set; }\n    public string? SubtitleId { get; set; }\n\n    public string? PeriodId { get; set; }\n\n    /// <summary>\n    /// URL\n    /// </summary>\n    public string Url { get; set; } = string.Empty;\n\n    /// <summary>\n    /// 原始URL\n    /// </summary>\n    public string OriginalUrl { get; set; } = string.Empty;\n\n    public Playlist? Playlist { get; set; }\n\n    public int SegmentsCount\n    {\n        get\n        {\n            return Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) : 0;\n        }\n    }\n\n    public string ToShortString()\n    {\n        var prefixStr = \"\";\n        var returnStr = \"\";\n        var encStr = string.Empty;\n\n        if (MediaType == Enum.MediaType.AUDIO)\n        {\n            prefixStr = $\"[deepskyblue3]Aud[/] {encStr}\";\n            var d = $\"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + \" Kbps\" : \"\")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + \"CH\" : \"\")} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n        else if (MediaType == Enum.MediaType.SUBTITLES)\n        {\n            prefixStr = $\"[deepskyblue3_1]Sub[/] {encStr}\";\n            var d = $\"{GroupId} | {Language} | {Name} | {Codecs} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n        else\n        {\n            prefixStr = $\"[aqua]Vid[/] {encStr}\";\n            var d = $\"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n\n        returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();\n        while (returnStr.Contains(\"|  |\"))\n        {\n            returnStr = returnStr.Replace(\"|  |\", \"|\");\n        }\n\n        return returnStr.TrimEnd().TrimEnd('|').TrimEnd();\n    }\n\n    public string ToShortShortString()\n    {\n        var prefixStr = \"\";\n        var returnStr = \"\";\n        var encStr = string.Empty;\n\n        if (MediaType == Enum.MediaType.AUDIO)\n        {\n            prefixStr = $\"[deepskyblue3]Aud[/] {encStr}\";\n            var d = $\"{(Bandwidth != null ? (Bandwidth / 1000) + \" Kbps\" : \"\")} | {Name} | {Language} | {(Channels != null ? Channels + \"CH\" : \"\")} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n        else if (MediaType == Enum.MediaType.SUBTITLES)\n        {\n            prefixStr = $\"[deepskyblue3_1]Sub[/] {encStr}\";\n            var d = $\"{Language} | {Name} | {Codecs} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n        else\n        {\n            prefixStr = $\"[aqua]Vid[/] {encStr}\";\n            var d = $\"{Resolution} | {Bandwidth / 1000} Kbps | {FrameRate} | {VideoRange} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n\n        returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();\n        while (returnStr.Contains(\"|  |\"))\n        {\n            returnStr = returnStr.Replace(\"|  |\", \"|\");\n        }\n\n        return returnStr.TrimEnd().TrimEnd('|').TrimEnd();\n    }\n\n    public override string ToString()\n    {\n        var prefixStr = \"\";\n        var returnStr = \"\";\n        var encStr = string.Empty;\n        var segmentsCountStr = SegmentsCount == 0 ? \"\" : (SegmentsCount > 1 ? $\"{SegmentsCount} Segments\" : $\"{SegmentsCount} Segment\");\n\n        // 增加加密标志\n        if (Playlist != null && Playlist.MediaParts.Any(m => m.MediaSegments.Any(s => s.EncryptInfo.Method != EncryptMethod.NONE)))\n        {\n            var ms = Playlist.MediaParts.SelectMany(m => m.MediaSegments.Select(s => s.EncryptInfo.Method)).Where(e => e != EncryptMethod.NONE).Distinct();\n            encStr = $\"[red]*{string.Join(\",\", ms).EscapeMarkup()}[/] \";\n        }\n\n        if (MediaType == Enum.MediaType.AUDIO)\n        {\n            prefixStr = $\"[deepskyblue3]Aud[/] {encStr}\";\n            var d = $\"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + \" Kbps\" : \"\")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + \"CH\" : \"\")} | {segmentsCountStr} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n        else if (MediaType == Enum.MediaType.SUBTITLES)\n        {\n            prefixStr = $\"[deepskyblue3_1]Sub[/] {encStr}\";\n            var d = $\"{GroupId} | {Language} | {Name} | {Codecs} | {Characteristics} | {segmentsCountStr} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n        else\n        {\n            prefixStr = $\"[aqua]Vid[/] {encStr}\";\n            var d = $\"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {segmentsCountStr} | {Role}\";\n            returnStr = d.EscapeMarkup();\n        }\n\n        returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();\n        while (returnStr.Contains(\"|  |\"))\n        {\n            returnStr = returnStr.Replace(\"|  |\", \"|\");\n        }\n\n        // 计算时长\n        if (Playlist != null)\n        {\n            var total = Playlist.TotalDuration;\n            returnStr += \" | ~\" + GlobalUtil.FormatTime((int)total);\n        }\n\n        return returnStr.TrimEnd().TrimEnd('|').TrimEnd();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/SubCue.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Entity;\n\npublic class SubCue\n{\n    public TimeSpan StartTime { get; set; }\n    public TimeSpan EndTime { get; set; }\n    public required string Payload { get; set; }\n    public required string Settings { get; set; }\n\n    public override bool Equals(object? obj)\n    {\n        return obj is SubCue cue &&\n               StartTime.Equals(cue.StartTime) &&\n               EndTime.Equals(cue.EndTime) &&\n               Payload == cue.Payload &&\n               Settings == cue.Settings;\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(StartTime, EndTime, Payload, Settings);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Entity/WebVttSub.cs",
    "content": "﻿using System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Common.Entity;\n\npublic partial class WebVttSub\n{\n    [GeneratedRegex(\"X-TIMESTAMP-MAP.*\")]\n    private static partial Regex TSMapRegex();\n    [GeneratedRegex(\"MPEGTS:(\\\\d+)\")]\n    private static partial Regex TSValueRegex();\n    [GeneratedRegex(\"\\\\s\")]\n    private static partial Regex SplitRegex();\n    [GeneratedRegex(@\"<c\\..*?>([\\s\\S]*?)<\\/c>\")]\n    private static partial Regex VttClassRegex();\n\n    public List<SubCue> Cues { get; set; } = [];\n    public long MpegtsTimestamp { get; set; } = 0L;\n\n    /// <summary>\n    /// 从字节数组解析WEBVTT\n    /// </summary>\n    /// <param name=\"textBytes\"></param>\n    /// <returns></returns>\n    public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L)\n    {\n        return Parse(Encoding.UTF8.GetString(textBytes), BaseTimestamp);\n    }\n\n    /// <summary>\n    /// 从字节数组解析WEBVTT\n    /// </summary>\n    /// <param name=\"textBytes\"></param>\n    /// <param name=\"encoding\"></param>\n    /// <returns></returns>\n    public static WebVttSub Parse(byte[] textBytes, Encoding encoding, long BaseTimestamp = 0L)\n    {\n        return Parse(encoding.GetString(textBytes), BaseTimestamp);\n    }\n\n    /// <summary>\n    /// 从字符串解析WEBVTT\n    /// </summary>\n    /// <param name=\"text\"></param>\n    /// <returns></returns>\n    public static WebVttSub Parse(string text, long BaseTimestamp = 0L)\n    {\n        if (!text.Trim().StartsWith(\"WEBVTT\"))\n            throw new Exception(\"Bad vtt!\");\n\n        text += Environment.NewLine;\n\n        var webSub = new WebVttSub();\n        var needPayload = false;\n        var timeLine = \"\";\n        var regex1 = TSMapRegex();\n\n        if (regex1.IsMatch(text))\n        {\n            var timestamp = TSValueRegex().Match(regex1.Match(text).Value).Groups[1].Value;\n            webSub.MpegtsTimestamp = Convert.ToInt64(timestamp);\n        }\n\n        var payloads = new List<string>();\n        foreach (var line in text.Split('\\n'))\n        {\n            if (line.Contains(\" --> \"))\n            {\n                needPayload = true;\n                timeLine = line.Trim();\n                continue;\n            }\n\n            if (!needPayload) continue;\n            \n            if (string.IsNullOrEmpty(line.Trim()))\n            {\n                var payload = string.Join(Environment.NewLine, payloads);\n                if (string.IsNullOrEmpty(payload.Trim())) continue; // 没获取到payload 跳过添加\n\n                var arr = SplitRegex().Split(timeLine.Replace(\"-->\", \"\")).Where(s => !string.IsNullOrEmpty(s)).ToList();\n                var startTime = ConvertToTS(arr[0]);\n                var endTime = ConvertToTS(arr[1]);\n                var style = arr.Count > 2 ? string.Join(\" \", arr.Skip(2)) : \"\";\n                webSub.Cues.Add(new SubCue()\n                {\n                    StartTime = startTime,\n                    EndTime = endTime,\n                    Payload = RemoveClassTag(string.Join(\"\", payload.Where(c => c != 8203))), // Remove Zero Width Space!\n                    Settings = style\n                });\n                payloads.Clear();\n                needPayload = false;\n            }\n            else\n            {\n                payloads.Add(line.Trim());\n            }\n        }\n\n        if (BaseTimestamp == 0) return webSub;\n        \n        foreach (var item in webSub.Cues)\n        {\n            if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)\n            {\n                item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp);\n                item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp);\n            }\n            else\n            {\n                break;\n            }\n        }\n\n        return webSub;\n    }\n\n    private static string RemoveClassTag(string text)\n    {\n        if (VttClassRegex().IsMatch(text))\n        {\n            return string.Join(Environment.NewLine, text.Split('\\n').Select(line => line.TrimEnd()).Select(line =>\n            {\n                return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + \" \"));\n            })).TrimEnd();\n        }\n        return text;\n    }\n\n    /// <summary>\n    /// 从另一个字幕中获取所有Cue，并加载此字幕中，且自动修正偏移\n    /// </summary>\n    /// <param name=\"webSub\"></param>\n    /// <returns></returns>\n    public WebVttSub AddCuesFromOne(WebVttSub webSub)\n    {\n        FixTimestamp(webSub, this.MpegtsTimestamp);\n        foreach (var item in webSub.Cues)\n        {\n            if (this.Cues.Contains(item)) continue;\n            \n            // 如果相差只有1ms，且payload相同，则拼接\n            var last = this.Cues.LastOrDefault();\n            if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload) \n            {\n                last.EndTime = item.EndTime;\n            }\n            else\n            {\n                this.Cues.Add(item);\n            }\n        }\n        return this;\n    }\n\n    private void FixTimestamp(WebVttSub sub, long baseTimestamp)\n    {\n        if (sub.MpegtsTimestamp == 0)\n        {\n            return;\n        }\n\n        // 确实存在时间轴错误的情况，才修复\n        if ((this.Cues.Count > 0 && sub.Cues.Count > 0 && sub.Cues.First().StartTime < this.Cues.Last().EndTime && sub.Cues.First().EndTime != this.Cues.Last().EndTime) || this.Cues.Count == 0)\n        {\n            // The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second\n            var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;\n            var offset = TimeSpan.FromSeconds(seconds);\n            // 当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒，而字幕起始却是2秒)，才修复\n            if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset)\n            {\n                foreach (var subCue in sub.Cues)\n                {\n                    subCue.StartTime += offset;\n                    subCue.EndTime += offset;\n                }\n            }\n        }\n    }\n\n    private IEnumerable<SubCue> GetCues()\n    {\n        return this.Cues.Where(c => !string.IsNullOrEmpty(c.Payload));\n    }\n\n    private static TimeSpan ConvertToTS(string str)\n    {\n        // 17.0s\n        if (str.EndsWith('s'))\n        {\n            double sec = Convert.ToDouble(str[..^1]);\n            return TimeSpan.FromSeconds(sec);\n        }\n\n        str = str.Replace(',', '.');\n        long time = 0;\n        string[] parts = str.Split('.');\n        if (parts.Length > 1)\n        {\n            time += Convert.ToInt32(parts.Last().PadRight(3, '0'));\n            str = parts.First();\n        }\n        var t = str.Split(':').Reverse().ToList();\n        for (int i = 0; i < t.Count; i++)\n        {\n            time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;\n        }\n        return TimeSpan.FromMilliseconds(time);\n    }\n\n    public override string ToString()\n    {\n        var sb = new StringBuilder();\n        foreach (var c in GetCues())  // 输出时去除空串\n        {\n            sb.AppendLine(c.StartTime.ToString(@\"hh\\:mm\\:ss\\.fff\") + \" --> \" + c.EndTime.ToString(@\"hh\\:mm\\:ss\\.fff\") + \" \" + c.Settings);\n            sb.AppendLine(c.Payload);\n            sb.AppendLine();\n        }\n        sb.AppendLine();\n        return sb.ToString();\n    }\n\n    /// <summary>\n    /// 字幕向前平移指定时间\n    /// </summary>\n    /// <param name=\"time\"></param>\n    public void LeftShiftTime(TimeSpan time)\n    {\n        foreach (var cue in this.Cues)\n        {\n            if (cue.StartTime.TotalSeconds - time.TotalSeconds > 0) cue.StartTime -= time;\n            else cue.StartTime = TimeSpan.FromSeconds(0);\n\n            if (cue.EndTime.TotalSeconds - time.TotalSeconds > 0) cue.EndTime -= time;\n            else cue.EndTime = TimeSpan.FromSeconds(0);\n        }\n    }\n\n    public string ToVtt()\n    {\n        return \"WEBVTT\" + Environment.NewLine + Environment.NewLine + ToString();\n    }\n\n    public string ToSrt()\n    {\n        StringBuilder sb = new StringBuilder();\n        int index = 1;\n        foreach (var c in GetCues())\n        {\n            sb.AppendLine($\"{index++}\");\n            sb.AppendLine(c.StartTime.ToString(@\"hh\\:mm\\:ss\\,fff\") + \" --> \" + c.EndTime.ToString(@\"hh\\:mm\\:ss\\,fff\"));\n            sb.AppendLine(c.Payload);\n            sb.AppendLine();\n        }\n        sb.AppendLine();\n\n        var srt = sb.ToString();\n\n        if (string.IsNullOrEmpty(srt.Trim()))\n        {\n            srt = \"1\\r\\n00:00:00,000 --> 00:00:01,000\"; // 空字幕\n        }\n\n        return srt;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/Choise.cs",
    "content": "﻿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",
    "content": "﻿namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum EncryptMethod\n{\n    NONE,\n    AES_128,\n    AES_128_ECB,\n    SAMPLE_AES,\n    SAMPLE_AES_CTR,\n    CENC,\n    CHACHA20,\n    UNKNOWN\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs",
    "content": "﻿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",
    "content": "﻿namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum MediaType\n{\n    AUDIO = 0,\n    VIDEO = 1,\n    SUBTITLES = 2,\n    CLOSED_CAPTIONS = 3\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Enum/RoleType.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Enum;\n\npublic enum RoleType\n{\n    Subtitle = 0,\n    Main = 1,\n    Alternate = 2,\n    Supplementary = 3,\n    Commentary = 4,\n    Dub = 5,\n    Description = 6,\n    Sign = 7,\n    Metadata = 8,\n    ForcedSubtitle = 9\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/JsonContext/JsonContext.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing System.Text.Json.Serialization;\n\nnamespace N_m3u8DL_RE.Common;\n\n[JsonSourceGenerationOptions(\n    WriteIndented = true,\n    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,\n    GenerationMode = JsonSourceGenerationMode.Metadata)]\n[JsonSerializable(typeof(MediaType))]\n[JsonSerializable(typeof(EncryptMethod))]\n[JsonSerializable(typeof(ExtractorType))]\n[JsonSerializable(typeof(Choise))]\n[JsonSerializable(typeof(StreamSpec))]\n[JsonSerializable(typeof(IOrderedEnumerable<StreamSpec>))]\n[JsonSerializable(typeof(IEnumerable<MediaSegment>))]\n[JsonSerializable(typeof(List<StreamSpec>))]\n[JsonSerializable(typeof(List<MediaSegment>))]\n[JsonSerializable(typeof(Dictionary<string, string>))]\ninternal partial class JsonContext : JsonSerializerContext { }"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/JsonConverter/BytesBase64Converter.cs",
    "content": "﻿using System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace N_m3u8DL_RE.Common.JsonConverter;\n\ninternal class BytesBase64Converter : JsonConverter<byte[]>\n{\n    public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetBytesFromBase64();\n\n    public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) => writer.WriteStringValue(Convert.ToBase64String(value));\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Log/CustomAnsiConsole.cs",
    "content": "﻿using System.Text;\nusing System.Text.RegularExpressions;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Common.Log;\n\npublic partial class NonAnsiWriter : TextWriter\n{\n    public override Encoding Encoding => Console.OutputEncoding;\n\n    private string? _lastOut = \"\";\n\n    public override void Write(char value)\n    {\n        Console.Write(value);\n    }\n\n    public override void Write(string? value)\n    {\n        if (_lastOut == value)\n        {\n            return;\n        }\n        _lastOut = value;\n        RemoveAnsiEscapeSequences(value);\n    }\n\n    private void RemoveAnsiEscapeSequences(string? input)\n    {\n        // Use regular expression to remove ANSI escape sequences\n        var output = MyRegex().Replace(input ?? \"\", \"\");\n        output = MyRegex1().Replace(output, \"\");\n        output = MyRegex2().Replace(output, \"\");\n        if (string.IsNullOrWhiteSpace(output))\n        {\n            return;\n        }\n        Console.Write(output);\n    }\n\n    [GeneratedRegex(@\"\\x1B\\[(\\d+;?)+m\")]\n    private static partial Regex MyRegex();\n    [GeneratedRegex(@\"\u001b\\[\\??\\d+[AKlh]\")]\n    private static partial Regex MyRegex1();\n    [GeneratedRegex(\"[\\r\\n] +\")]\n    private static partial Regex MyRegex2();\n}\n\n/// <summary>\n/// A console capable of writing ANSI escape sequences.\n/// </summary>\npublic static class CustomAnsiConsole\n{\n    public static IAnsiConsole Console { get; set; } = AnsiConsole.Console;\n\n    public static void InitConsole(bool forceAnsi, bool noAnsiColor)\n    {\n        if (forceAnsi)\n        {\n            var ansiConsoleSettings = new AnsiConsoleSettings();\n            if (noAnsiColor)\n            {\n                ansiConsoleSettings.Out = new AnsiConsoleOutput(new NonAnsiWriter());\n            }\n\n            ansiConsoleSettings.Interactive = InteractionSupport.Yes;\n            ansiConsoleSettings.Ansi = AnsiSupport.Yes;\n            Console = AnsiConsole.Create(ansiConsoleSettings);\n            Console.Profile.Width = int.MaxValue;\n        }\n        else\n        {\n            var ansiConsoleSettings = new AnsiConsoleSettings();\n            if (noAnsiColor)\n            {\n                ansiConsoleSettings.Out = new AnsiConsoleOutput(new NonAnsiWriter());\n            }\n            Console = AnsiConsole.Create(ansiConsoleSettings);\n        }\n    }\n\n    /// <summary>\n    /// Writes the specified markup to the console.\n    /// </summary>\n    /// <param name=\"value\">The value to write.</param>\n    public static void Markup(string value)\n    {\n        Console.Markup(value);\n    }\n\n    /// <summary>\n    /// Writes the specified markup, followed by the current line terminator, to the console.\n    /// </summary>\n    /// <param name=\"value\">The value to write.</param>\n    public static void MarkupLine(string value)\n    {\n        Console.MarkupLine(value);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Log/LogLevel.cs",
    "content": "﻿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",
    "content": "﻿using Spectre.Console;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Common.Log;\n\npublic static partial class Logger\n{\n    [GeneratedRegex(\"{}\")]\n    private static partial Regex VarsRepRegex();\n\n    /// <summary>\n    /// 日志级别，默认为INFO\n    /// </summary>\n    public static LogLevel LogLevel { get; set; } = LogLevel.INFO;\n\n    /// <summary>\n    /// 是否写出日志文件\n    /// </summary>\n    public static bool IsWriteFile { get; set; } = true;\n\n    /// <summary>\n    /// 本次运行日志文件所在位置\n    /// </summary>\n    public static string? LogFilePath { get; set; }\n\n    // 读写锁\n    static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();\n\n    public static void InitLogFile()\n    {\n        if (!IsWriteFile) return;\n\n        try\n        {\n            var logDir = Path.GetDirectoryName(LogFilePath) ?? (Path.GetDirectoryName(Environment.ProcessPath) + \"/Logs\");\n            if (!Directory.Exists(logDir))\n            {\n                Directory.CreateDirectory(logDir);\n            }\n\n            var now = DateTime.Now;\n            if (string.IsNullOrEmpty(LogFilePath))\n            {\n                LogFilePath = Path.Combine(logDir, now.ToString(\"yyyy-MM-dd_HH-mm-ss-fff\") + \".log\");\n                int index = 1;\n                var fileName = Path.GetFileNameWithoutExtension(LogFilePath);\n                // 若文件存在则加序号\n                while (File.Exists(LogFilePath))\n                {\n                    LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $\"{fileName}-{index++}.log\");\n                }\n            }\n\n            string init = \"LOG \" + now.ToString(\"yyyy/MM/dd\") + Environment.NewLine\n                          + \"Save Path: \" + Path.GetDirectoryName(LogFilePath) + Environment.NewLine\n                          + \"Task Start: \" + now.ToString(\"yyyy/MM/dd HH:mm:ss\") + Environment.NewLine\n                          + \"Task CommandLine: \" + Environment.CommandLine;\n            init += $\"{Environment.NewLine}{Environment.NewLine}\";\n            File.WriteAllText(LogFilePath, init, Encoding.UTF8);\n        }\n        catch (Exception ex)\n        {\n            Error($\"Init log failed! {ex.Message.RemoveMarkup()}\");\n        }\n    }\n\n    private static string GetCurrTime()\n    {\n        return DateTime.Now.ToString(\"HH:mm:ss.fff\");\n    }\n\n    private static void HandleLog(string write, string subWrite = \"\")\n    {\n        try\n        {\n            if (subWrite == \"\")\n            {\n                CustomAnsiConsole.MarkupLine(write);\n            }\n            else\n            {\n                CustomAnsiConsole.Markup(write);\n                Console.WriteLine(subWrite);\n            }\n\n            if (!IsWriteFile || !File.Exists(LogFilePath)) return;\n            \n            var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();\n            try\n            {\n                // 进入写入\n                LogWriteLock.EnterWriteLock();\n                using (StreamWriter sw = File.AppendText(LogFilePath))\n                {\n                    sw.WriteLine(plain);\n                }\n            }\n            finally\n            {\n                // 释放占用\n                LogWriteLock.ExitWriteLock();\n            }\n        }\n        catch (Exception)\n        {\n            Console.WriteLine(\"Failed to write: \" + write);\n        }\n    }\n\n    private static string ReplaceVars(string data, params object[] ps)\n    {\n        for (int i = 0; i < ps.Length; i++)\n        {\n            data = VarsRepRegex().Replace(data, $\"{ps[i]}\", 1);\n        }\n\n        return data;\n    }\n\n    public static void Info(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.INFO) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline #548c26]INFO[/] : \";\n        HandleLog(write, data);\n    }\n\n    public static void InfoMarkUp(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.INFO) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline #548c26]INFO[/] : \" + data;\n        HandleLog(write);\n    }\n\n    public static void Debug(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.DEBUG) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline grey]DEBUG[/]: \";\n        HandleLog(write, data);\n    }\n\n    public static void DebugMarkUp(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.DEBUG) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline grey]DEBUG[/]: \" + data;\n        HandleLog(write);\n    }\n\n    public static void Warn(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.WARN) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline #a89022]WARN[/] : \";\n        HandleLog(write, data);\n    }\n\n    public static void WarnMarkUp(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.WARN) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline #a89022]WARN[/] : \" + data;\n        HandleLog(write);\n    }\n\n    public static void Error(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.ERROR) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline red1]ERROR[/]: \";\n        HandleLog(write, data);\n    }\n\n    public static void ErrorMarkUp(string data, params object[] ps)\n    {\n        if (LogLevel < LogLevel.ERROR) return;\n        \n        data = ReplaceVars(data, ps);\n        var write = GetCurrTime() + \" \" + \"[underline red1]ERROR[/]: \" + data;\n        HandleLog(write);\n    }\n\n    public static void ErrorMarkUp(Exception exception)\n    {\n        string data = exception.Message.EscapeMarkup();\n        if (LogLevel >= LogLevel.ERROR)\n        {\n            data = exception.ToString().EscapeMarkup();\n        }\n\n        ErrorMarkUp(data);\n    }\n\n    /// <summary>\n    /// This thing will only write to the log file.\n    /// </summary>\n    /// <param name=\"data\"></param>\n    /// <param name=\"ps\"></param>\n    public static void Extra(string data, params object[] ps)\n    {\n        if (!IsWriteFile || !File.Exists(LogFilePath)) return;\n        \n        data = ReplaceVars(data, ps);\n        var plain = GetCurrTime() + \" \" + \"EXTRA: \" + data.RemoveMarkup();\n        try\n        {\n            // 进入写入\n            LogWriteLock.EnterWriteLock();\n            using (StreamWriter sw = File.AppendText(LogFilePath))\n            {\n                sw.WriteLine(plain, Encoding.UTF8);\n            }\n        }\n        finally\n        {\n            // 释放占用\n            LogWriteLock.ExitWriteLock();\n        }\n    }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/N_m3u8DL-RE.Common.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n\t<OutputType>library</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>N_m3u8DL_RE.Common</RootNamespace>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <LangVersion>13.0</LangVersion>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n\t  <PackageReference Include=\"Spectre.Console\" Version=\"0.52.1-preview.0.5\" />\n  </ItemGroup>\n\t\n</Project>\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Resource/ResString.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Resource;\n\npublic static class ResString\n{\n    public static string CurrentLoc { get; set; } = \"en-US\";\n\n    public static readonly string ReLiveTs = \"<RE_LIVE_TS>\";\n    public static readonly string ReBinaryData = \"<RE_BINARY_DATA>\";\n    public static string singleFileRealtimeDecryptWarn => GetText(\"singleFileRealtimeDecryptWarn\");\n    public static string singleFileSplitWarn => GetText(\"singleFileSplitWarn\");\n    public static string customRangeWarn => GetText(\"customRangeWarn\");\n    public static string customRangeFound => GetText(\"customRangeFound\");\n    public static string customAdKeywordsFound => GetText(\"customAdKeywordsFound\");\n    public static string customRangeInvalid => GetText(\"customRangeInvalid\");\n    public static string consoleRedirected => GetText(\"consoleRedirected\");\n    public static string autoBinaryMerge => GetText(\"autoBinaryMerge\");\n    public static string autoBinaryMerge2 => GetText(\"autoBinaryMerge2\");\n    public static string autoBinaryMerge3 => GetText(\"autoBinaryMerge3\");\n    public static string autoBinaryMerge4 => GetText(\"autoBinaryMerge4\");\n    public static string autoBinaryMerge5 => GetText(\"autoBinaryMerge5\");\n    public static string autoBinaryMerge6 => GetText(\"autoBinaryMerge6\");\n    public static string badM3u8 => GetText(\"badM3u8\");\n    public static string binaryMerge => GetText(\"binaryMerge\");\n    public static string checkingLast => GetText(\"checkingLast\");\n    public static string cmd_appendUrlParams => GetText(\"cmd_appendUrlParams\");\n    public static string cmd_autoSelect => GetText(\"cmd_autoSelect\");\n    public static string cmd_disableUpdateCheck => GetText(\"cmd_disableUpdateCheck\");\n    public static string cmd_binaryMerge => GetText(\"cmd_binaryMerge\");\n    public static string cmd_useFFmpegConcatDemuxer => GetText(\"cmd_useFFmpegConcatDemuxer\");\n    public static string cmd_checkSegmentsCount => GetText(\"cmd_checkSegmentsCount\");\n    public static string cmd_decryptionBinaryPath => GetText(\"cmd_decryptionBinaryPath\");\n    public static string cmd_delAfterDone => GetText(\"cmd_delAfterDone\");\n    public static string cmd_ffmpegBinaryPath => GetText(\"cmd_ffmpegBinaryPath\");\n    public static string cmd_mkvmergeBinaryPath => GetText(\"cmd_mkvmergeBinaryPath\");\n    public static string cmd_baseUrl => GetText(\"cmd_baseUrl\");\n    public static string cmd_maxSpeed => GetText(\"cmd_maxSpeed\");\n    public static string cmd_adKeyword => GetText(\"cmd_adKeyword\");\n    public static string cmd_moreHelp => GetText(\"cmd_moreHelp\");\n    public static string cmd_header => GetText(\"cmd_header\");\n    public static string cmd_muxImport => GetText(\"cmd_muxImport\");\n    public static string cmd_muxImport_more => GetText(\"cmd_muxImport_more\");\n    public static string cmd_selectVideo => GetText(\"cmd_selectVideo\");\n    public static string cmd_dropVideo => GetText(\"cmd_dropVideo\");\n    public static string cmd_selectVideo_more => GetText(\"cmd_selectVideo_more\");\n    public static string cmd_selectAudio => GetText(\"cmd_selectAudio\");\n    public static string cmd_dropAudio => GetText(\"cmd_dropAudio\");\n    public static string cmd_selectAudio_more => GetText(\"cmd_selectAudio_more\");\n    public static string cmd_selectSubtitle => GetText(\"cmd_selectSubtitle\");\n    public static string cmd_dropSubtitle => GetText(\"cmd_dropSubtitle\");\n    public static string cmd_selectSubtitle_more => GetText(\"cmd_selectSubtitle_more\");\n    public static string cmd_custom_range => GetText(\"cmd_custom_range\");\n    public static string cmd_customHLSMethod => GetText(\"cmd_customHLSMethod\");\n    public static string cmd_customHLSKey => GetText(\"cmd_customHLSKey\");\n    public static string cmd_customHLSIv => GetText(\"cmd_customHLSIv\");\n    public static string cmd_Input => GetText(\"cmd_Input\");\n    public static string cmd_forceAnsiConsole => GetText(\"cmd_forceAnsiConsole\");\n    public static string cmd_noAnsiColor => GetText(\"cmd_noAnsiColor\");\n    public static string cmd_keys => GetText(\"cmd_keys\");\n    public static string cmd_keyText => GetText(\"cmd_keyText\");\n    public static string cmd_loadKeyFailed => GetText(\"cmd_loadKeyFailed\");\n    public static string cmd_logLevel => GetText(\"cmd_logLevel\");\n    public static string cmd_MP4RealTimeDecryption => GetText(\"cmd_MP4RealTimeDecryption\");\n    public static string cmd_saveDir => GetText(\"cmd_saveDir\");\n    public static string cmd_saveName => GetText(\"cmd_saveName\");\n    public static string cmd_savePattern => GetText(\"cmd_savePattern\");\n    public static string cmd_logFilePath => GetText(\"cmd_logFilePath\");\n    public static string cmd_skipDownload => GetText(\"cmd_skipDownload\");\n    public static string cmd_noDateInfo => GetText(\"cmd_noDateInfo\");\n    public static string cmd_noLog => GetText(\"cmd_noLog\");\n    public static string cmd_allowHlsMultiExtMap => GetText(\"cmd_allowHlsMultiExtMap\");\n    public static string cmd_skipMerge => GetText(\"cmd_skipMerge\");\n    public static string cmd_subFormat => GetText(\"cmd_subFormat\");\n    public static string cmd_subOnly => GetText(\"cmd_subOnly\");\n    public static string cmd_subtitleFix => GetText(\"cmd_subtitleFix\");\n    public static string cmd_threadCount => GetText(\"cmd_threadCount\");\n    public static string cmd_downloadRetryCount => GetText(\"cmd_downloadRetryCount\");\n    public static string cmd_httpRequestTimeout => GetText(\"cmd_httpRequestTimeout\");\n    public static string cmd_tmpDir => GetText(\"cmd_tmpDir\");\n    public static string cmd_uiLanguage => GetText(\"cmd_uiLanguage\");\n    public static string cmd_urlProcessorArgs => GetText(\"cmd_urlProcessorArgs\");\n    public static string cmd_useShakaPackager => GetText(\"cmd_useShakaPackager\");\n    public static string cmd_decryptionEngine => GetText(\"cmd_decryptionEngine\");\n    public static string cmd_concurrentDownload => GetText(\"cmd_concurrentDownload\");\n    public static string cmd_useSystemProxy => GetText(\"cmd_useSystemProxy\");\n    public static string cmd_customProxy => GetText(\"cmd_customProxy\");\n    public static string cmd_customRange => GetText(\"cmd_customRange\");\n    public static string cmd_liveKeepSegments => GetText(\"cmd_liveKeepSegments\");\n    public static string cmd_livePipeMux => GetText(\"cmd_livePipeMux\");\n    public static string cmd_liveRecordLimit => GetText(\"cmd_liveRecordLimit\");\n    public static string cmd_taskStartAt => GetText(\"cmd_taskStartAt\");\n    public static string cmd_liveWaitTime => GetText(\"cmd_liveWaitTime\");\n    public static string cmd_liveTakeCount => GetText(\"cmd_liveTakeCount\");\n    public static string cmd_liveFixVttByAudio => GetText(\"cmd_liveFixVttByAudio\");\n    public static string cmd_liveRealTimeMerge => GetText(\"cmd_liveRealTimeMerge\");\n    public static string cmd_livePerformAsVod => GetText(\"cmd_livePerformAsVod\");\n    public static string cmd_muxAfterDone => GetText(\"cmd_muxAfterDone\");\n    public static string cmd_muxAfterDone_more => GetText(\"cmd_muxAfterDone_more\");\n    public static string cmd_writeMetaJson => GetText(\"cmd_writeMetaJson\");\n    public static string liveLimit => GetText(\"liveLimit\");\n    public static string realTimeDecMessage => GetText(\"realTimeDecMessage\");\n    public static string liveLimitReached => GetText(\"liveLimitReached\");\n    public static string saveName => GetText(\"saveName\");\n    public static string taskStartAt => GetText(\"taskStartAt\");\n    public static string namedPipeCreated => GetText(\"namedPipeCreated\");\n    public static string namedPipeMux => GetText(\"namedPipeMux\");\n    public static string partMerge => GetText(\"partMerge\");\n    public static string fetch => GetText(\"fetch\");\n    public static string ffmpegMerge => GetText(\"ffmpegMerge\");\n    public static string ffmpegNotFound => GetText(\"ffmpegNotFound\");\n    public static string mkvmergeNotFound => GetText(\"mkvmergeNotFound\");\n    public static string mp4decryptNotFound => GetText(\"mp4decryptNotFound\");\n    public static string shakaPackagerNotFound => GetText(\"shakaPackagerNotFound\");\n    public static string fixingTTML => GetText(\"fixingTTML\");\n    public static string fixingTTMLmp4 => GetText(\"fixingTTMLmp4\");\n    public static string fixingVTT => GetText(\"fixingVTT\");\n    public static string fixingVTTmp4 => GetText(\"fixingVTTmp4\");\n    public static string keyProcessorNotFound => GetText(\"keyProcessorNotFound\");\n    public static string liveFound => GetText(\"liveFound\");\n    public static string loadingUrl => GetText(\"loadingUrl\");\n    public static string masterM3u8Found => GetText(\"masterM3u8Found\");\n    public static string allowHlsMultiExtMap => GetText(\"allowHlsMultiExtMap\");\n    public static string matchDASH => GetText(\"matchDASH\");\n    public static string matchMSS => GetText(\"matchMSS\");\n    public static string matchTS => GetText(\"matchTS\");\n    public static string matchHLS => GetText(\"matchHLS\");\n    public static string matchBinaryData => GetText(\"matchBinaryData\");\n    public static string notSupported => GetText(\"notSupported\");\n    public static string parsingStream => GetText(\"parsingStream\");\n    public static string promptChoiceText => GetText(\"promptChoiceText\");\n    public static string promptInfo => GetText(\"promptInfo\");\n    public static string promptTitle => GetText(\"promptTitle\");\n    public static string readingInfo => GetText(\"readingInfo\");\n    public static string searchKey => GetText(\"searchKey\");\n    public static string decryptionFailed => GetText(\"decryptionFailed\");\n    public static string segmentCountCheckNotPass => GetText(\"segmentCountCheckNotPass\");\n    public static string selectedStream => GetText(\"selectedStream\");\n    public static string startDownloading => GetText(\"startDownloading\");\n    public static string streamsInfo => GetText(\"streamsInfo\");\n    public static string writeJson => GetText(\"writeJson\");\n    public static string noStreamsToDownload => GetText(\"noStreamsToDownload\");\n    public static string loadUrlFailed => GetText(\"loadUrlFailed\");\n    public static string newVersionFound => GetText(\"newVersionFound\");\n    public static string processImageSub => GetText(\"processImageSub\");\n\n    private static string GetText(string key)\n    {\n        if (!StaticText.LANG_DIC.TryGetValue(key, out var textObj))\n            return \"<...LANG TEXT MISSING...>\";\n\n        if (CurrentLoc is \"zh-CN\" or \"zh-SG\" or \"zh-Hans\")\n            return textObj.ZH_CN;\n        return CurrentLoc.StartsWith(\"zh-\") ? textObj.ZH_TW : textObj.EN_US;\n    }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Resource/StaticText.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Resource;\n\ninternal static class StaticText\n{\n    public static readonly Dictionary<string, TextContainer> LANG_DIC = new()\n    {\n        [\"singleFileSplitWarn\"] = new TextContainer\n        (\n            zhCN: \"整段文件已被自动切割为小分片以加速下载\",\n            zhTW: \"整段文件已被自動切割為小分片以加速下載\",\n            enUS: \"The entire file has been cut into small segments to accelerate\"\n        ),\n        [\"singleFileRealtimeDecryptWarn\"] = new TextContainer\n        (\n            zhCN: \"实时解密已被强制关闭\",\n            zhTW: \"即時解密已被強制關閉\",\n            enUS: \"Real-time decryption has been disabled\"\n        ),\n        [\"cmd_forceAnsiConsole\"] = new TextContainer\n        (\n            zhCN: \"强制认定终端为支持ANSI且可交互的终端\",\n            zhTW: \"強制認定終端為支援ANSI且可交往的終端\",\n            enUS: \"Force assuming the terminal is ANSI-compatible and interactive\"\n        ),\n        [\"cmd_noAnsiColor\"] = new TextContainer\n        (\n            zhCN: \"去除ANSI颜色\",\n            zhTW: \"關閉ANSI顏色\",\n            enUS: \"Remove ANSI colors\"\n        ),\n        [\"customRangeWarn\"] = new TextContainer\n        (\n            zhCN: \"请注意，自定义下载范围有时会导致音画不同步\",\n            zhTW: \"請注意，自定義下載範圍有時會導致音畫不同步\",\n            enUS: \"Please note that custom range may sometimes result in audio and video being out of sync\"\n        ),\n        [\"customRangeInvalid\"] = new TextContainer\n        (\n            zhCN: \"自定义下载范围无效\",\n            zhTW: \"自定義下載範圍無效\",\n            enUS: \"User customed range invalid\"\n        ),\n        [\"customAdKeywordsFound\"] = new TextContainer\n        (\n            zhCN: \"用户自定义广告分片URL关键字：\",\n            zhTW: \"用戶自定義廣告分片URL關鍵字：\",\n            enUS: \"User customed Ad keyword: \"\n        ),\n        [\"customRangeFound\"] = new TextContainer\n        (\n            zhCN: \"用户自定义下载范围：\",\n            zhTW: \"用戶自定義下載範圍：\",\n            enUS: \"User customed range: \"\n        ),\n        [\"consoleRedirected\"] = new TextContainer\n        (\n            zhCN: \"输出被重定向, 将清除ANSI颜色\",\n            zhTW: \"輸出被重定向, 將清除ANSI顏色\",\n            enUS: \"Output is redirected, ANSI colors are cleared.\"\n        ),\n        [\"processImageSub\"] = new TextContainer\n        (\n            zhCN: \"正在处理图形字幕\",\n            zhTW: \"正在處理圖形字幕\",\n            enUS: \"Processing Image Sub\"\n        ),\n        [\"newVersionFound\"] = new TextContainer\n        (\n            zhCN: \"检测到新版本，请尽快升级！\",\n            zhTW: \"檢測到新版本，請盡快升級！\",\n            enUS: \"New version detected!\"\n        ),\n        [\"namedPipeCreated\"] = new TextContainer\n        (\n            zhCN: \"已创建命名管道：\",\n            zhTW: \"已創建命名管道：\",\n            enUS: \"Named pipe created: \"\n        ),\n        [\"namedPipeMux\"] = new TextContainer\n        (\n            zhCN: \"通过命名管道混流到\",\n            zhTW: \"通過命名管道混流到\",\n            enUS: \"Mux with named pipe, to\"\n        ),\n        [\"taskStartAt\"] = new TextContainer\n        (\n            zhCN: \"程序将等待，直到：\",\n            zhTW: \"程序將等待，直到：\",\n            enUS: \"The program will wait until: \"\n        ),\n        [\"autoBinaryMerge\"] = new TextContainer\n        (\n            zhCN: \"检测到fMP4，自动开启二进制合并\",\n            zhTW: \"檢測到fMP4，自動開啟二進位制合併\",\n            enUS: \"fMP4 is detected, binary merging is automatically enabled\"\n        ),\n        [\"autoBinaryMerge2\"] = new TextContainer\n        (\n            zhCN: \"检测到杜比视界内容，自动开启二进制合并\",\n            zhTW: \"檢測到杜比視界內容，自動開啟二進位制合併\",\n            enUS: \"Dolby Vision content is detected, binary merging is automatically enabled\"\n        ),\n        [\"autoBinaryMerge3\"] = new TextContainer\n        (\n            zhCN: \"检测到无法识别的加密方式，自动开启二进制合并\",\n            zhTW: \"檢測到無法識別的加密方式，自動開啟二進位制合併\",\n            enUS: \"An unrecognized encryption method is detected, binary merging is automatically enabled\"\n        ),\n        [\"autoBinaryMerge4\"] = new TextContainer\n        (\n            zhCN: \"检测到CENC加密方式，自动开启二进制合并\",\n            zhTW: \"檢測到CENC加密方式，自動開啟二進位制合併\",\n            enUS: \"When CENC encryption is detected, binary merging is automatically enabled\"\n        ),\n        [\"autoBinaryMerge5\"] = new TextContainer\n        (\n            zhCN: \"检测到杜比视界内容，混流功能已禁用\",\n            zhTW: \"檢測到杜比視界內容，混流功能已禁用\",\n            enUS: \"Dolby Vision content is detected, mux after done is automatically disabled\"\n        ),\n        [\"autoBinaryMerge6\"] = new TextContainer\n        (\n            zhCN: \"你已开启下载完成后混流，自动开启二进制合并\",\n            zhTW: \"你已開啟下載完成後混流，自動開啟二進制合併\",\n            enUS: \"MuxAfterDone is detected, binary merging is automatically enabled\"\n        ),\n        [\"badM3u8\"] = new TextContainer\n        (\n            zhCN: \"错误的m3u8\",\n            zhTW: \"錯誤的m3u8\",\n            enUS: \"Bad m3u8\"\n        ),\n        [\"binaryMerge\"] = new TextContainer\n        (\n            zhCN: \"二进制合并中...\",\n            zhTW: \"二進位制合併中...\",\n            enUS: \"Binary merging...\"\n        ),\n        [\"checkingLast\"] = new TextContainer\n        (\n            zhCN: \"验证最后一个分片有效性\",\n            zhTW: \"驗證最後一個分片有效性\",\n            enUS: \"Verifying the validity of the last segment\"\n        ),\n        [\"cmd_baseUrl\"] = new TextContainer\n        (\n            zhCN: \"设置BaseURL\",\n            zhTW: \"設置BaseURL\",\n            enUS: \"Set BaseURL\"\n        ),\n        [\"cmd_maxSpeed\"] = new TextContainer\n        (\n            zhCN: \"设置限速，单位支持 Mbps 或 Kbps，如：15M 100K\",\n            zhTW: \"設置限速，單位支持 Mbps 或 Kbps，如：15M 100K\",\n            enUS: \"Set speed limit, Mbps or Kbps, for example: 15M 100K.\"\n        ),\n        [\"cmd_noDateInfo\"] = new TextContainer\n        (\n            zhCN: \"混流时不写入日期信息\",\n            zhTW: \"混流時不寫入日期訊息\",\n            enUS: \"Date information is not written during muxing\"\n        ),\n        [\"cmd_noLog\"] = new TextContainer\n        (\n            zhCN: \"关闭日志文件输出\",\n            zhTW: \"關閉日誌文件輸出\",\n            enUS: \"Disable log file output\"\n        ),\n        [\"cmd_allowHlsMultiExtMap\"] = new TextContainer\n        (\n            zhCN: \"允许HLS中的多个#EXT-X-MAP(实验性)\",\n            zhTW: \"允許HLS中的多個#EXT-X-MAP(實驗性)\",\n            enUS: \"Allow multiple #EXT-X-MAP in HLS (experimental)\"\n        ),\n        [\"cmd_appendUrlParams\"] = new TextContainer\n        (\n            zhCN: \"将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com\",\n            zhTW: \"將輸入Url的Params添加至分片, 對某些網站很有用, 例如 kakao.com\",\n            enUS: \"Add Params of input Url to segments, useful for some websites, such as kakao.com\"\n        ),\n        [\"cmd_autoSelect\"] = new TextContainer\n        (\n            zhCN: \"自动选择所有类型的最佳轨道\",\n            zhTW: \"自動選擇所有類型的最佳軌道\",\n            enUS: \"Automatically selects the best tracks of all types\"\n        ),\n        [\"cmd_disableUpdateCheck\"] = new TextContainer\n        (\n            zhCN: \"禁用版本更新检测\",\n            zhTW: \"禁用版本更新檢測\",\n            enUS: \"Disable version update check\"\n        ),\n        [\"cmd_binaryMerge\"] = new TextContainer\n        (\n            zhCN: \"二进制合并\",\n            zhTW: \"二進位制合併\",\n            enUS: \"Binary merge\"\n        ),\n        [\"cmd_useFFmpegConcatDemuxer\"] = new TextContainer\n        (\n            zhCN: \"使用 ffmpeg 合并时，使用 concat 分离器而非 concat 协议\",\n            zhTW: \"使用 ffmpeg 合併時，使用 concat 分離器而非 concat 協議\",\n            enUS: \"When merging with ffmpeg, use the concat demuxer instead of the concat protocol\"\n        ),\n        [\"cmd_checkSegmentsCount\"] = new TextContainer\n        (\n            zhCN: \"检测实际下载的分片数量和预期数量是否匹配\",\n            zhTW: \"檢測實際下載的分片數量和預期數量是否匹配\",\n            enUS: \"Check if the actual number of segments downloaded matches the expected number\"\n        ),\n        [\"cmd_downloadRetryCount\"] = new TextContainer\n        (\n            zhCN: \"每个分片下载异常时的重试次数\",\n            zhTW: \"每個分片下載異常時的重試次數\",\n            enUS: \"The number of retries when download segment error\"\n        ),\n        [\"cmd_httpRequestTimeout\"] = new TextContainer\n        (\n            zhCN: \"HTTP请求的超时时间(秒)\",\n            zhTW: \"HTTP請求的超時時間(秒)\",\n            enUS: \"Timeout duration for HTTP requests (in seconds)\"\n        ),\n        [\"cmd_decryptionBinaryPath\"] = new TextContainer\n        (\n            zhCN: @\"MP4解密所用工具的全路径, 例如 C:\\Tools\\mp4decrypt.exe\",\n            zhTW: @\"MP4解密所用工具的全路徑, 例如 C:\\Tools\\mp4decrypt.exe\",\n            enUS: @\"Full path to the tool used for MP4 decryption, like C:\\Tools\\mp4decrypt.exe\"\n        ),\n        [\"cmd_delAfterDone\"] = new TextContainer\n        (\n            zhCN: \"完成后删除临时文件\",\n            zhTW: \"完成後刪除臨時文件\",\n            enUS: \"Delete temporary files when done\"\n        ),\n        [\"cmd_ffmpegBinaryPath\"] = new TextContainer\n        (\n            zhCN: @\"ffmpeg可执行程序全路径, 例如 C:\\Tools\\ffmpeg.exe\",\n            zhTW: @\"ffmpeg可執行程序全路徑, 例如 C:\\Tools\\ffmpeg.exe\",\n            enUS: @\"Full path to the ffmpeg binary, like C:\\Tools\\ffmpeg.exe\"\n        ),\n        [\"cmd_mkvmergeBinaryPath\"] = new TextContainer\n        (\n            zhCN: @\"mkvmerge可执行程序全路径, 例如 C:\\Tools\\mkvmerge.exe\",\n            zhTW: @\"mkvmerge可執行程序全路徑, 例如 C:\\Tools\\mkvmerge.exe\",\n            enUS: @\"Full path to the mkvmerge binary, like C:\\Tools\\mkvmerge.exe\"\n        ),\n        [\"cmd_liveFixVttByAudio\"] = new TextContainer\n        (\n            zhCN: \"通过读取音频文件的起始时间修正VTT字幕\",\n            zhTW: \"透過讀取音訊檔案的起始時間修正VTT字幕\",\n            enUS: \"Correct VTT sub by reading the start time of the audio file\"\n        ),\n        [\"cmd_header\"] = new TextContainer\n        (\n            zhCN: \"为HTTP请求设置特定的请求头, 例如:\\r\\n-H \\\"Cookie: mycookie\\\" -H \\\"User-Agent: iOS\\\"\",\n            zhTW: \"為HTTP請求設置特定的請求頭, 例如:\\r\\n-H \\\"Cookie: mycookie\\\" -H \\\"User-Agent: iOS\\\"\",\n            enUS: \"Pass custom header(s) to server, Example:\\r\\n-H \\\"Cookie: mycookie\\\" -H \\\"User-Agent: iOS\\\"\"\n        ),\n        [\"cmd_Input\"] = new TextContainer\n        (\n            zhCN: \"链接或文件\",\n            zhTW: \"連結或文件\",\n            enUS: \"Input Url or File\"\n        ),\n        [\"cmd_keys\"] = new TextContainer\n        (\n            zhCN: \"设置解密密钥, 程序调用mp4decrpyt/shaka-packager/ffmpeg进行解密. 格式:\\r\\n--key KID1:KEY1 --key KID2:KEY2\\r\\n对于KEY相同的情况可以直接输入 --key KEY\",\n            zhTW: \"設置解密密鑰, 程序調用mp4decrpyt/shaka-packager/ffmpeg進行解密. 格式:\\r\\n--key KID1:KEY1 --key KID2:KEY2\\r\\n對於KEY相同的情況可以直接輸入 --key KEY\",\n            enUS: \"Set decryption key(s) to mp4decrypt/shaka-packager/ffmpeg. format:\\r\\n--key KID1:KEY1 --key KID2:KEY2\\r\\nor use --key KEY if all tracks share the same key.\"\n        ),\n        [\"cmd_keyText\"] = new TextContainer\n        (\n            zhCN: \"设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)\",\n            zhTW: \"設置密鑰文件,程序將從文件中按KID搜尋KEY以解密.(不建議使用特大文件)\",\n            enUS: \"Set the kid-key file, the program will search the KEY with KID from the file.(Very large file are not recommended)\"\n        ),\n        [\"cmd_loadKeyFailed\"] = new TextContainer\n        (\n            zhCN: \"获取KEY失败，忽略读取.\",\n            zhTW: \"獲取KEY失敗，忽略讀取.\",\n            enUS: \"Failed to get KEY, ignore.\"\n        ),\n        [\"cmd_logLevel\"] = new TextContainer\n        (\n            zhCN: \"设置日志级别\",\n            zhTW: \"設置日誌級別\",\n            enUS: \"Set log level\"\n        ),\n        [\"cmd_MP4RealTimeDecryption\"] = new TextContainer\n        (\n            zhCN: \"实时解密MP4分片\",\n            zhTW: \"即時解密MP4分片\",\n            enUS: \"Decrypt MP4 segments in real time\"\n        ),\n        [\"cmd_saveDir\"] = new TextContainer\n        (\n            zhCN: \"设置输出目录\",\n            zhTW: \"設置輸出目錄\",\n            enUS: \"Set output directory\"\n        ),\n        [\"cmd_saveName\"] = new TextContainer\n        (\n            zhCN: \"设置保存文件名\",\n            zhTW: \"設置保存檔案名\",\n            enUS: \"Set output filename\"\n        ),\n        [\"cmd_savePattern\"] = new TextContainer\n        (\n            zhCN: \"设置保存文件命名模板, 支持使用变量: \\n\" +\n                  \"<SaveName>, <Id>, <Codecs>, <Language>, <Resolution>, \\n\" +\n                  \"<Bandwidth>, <MediaType>, <Channels>, <FrameRate>, \\n\" +\n                  \"<VideoRange>, <GroupId>, <Ext>\\n\" +\n                  \"示例: --save-pattern \\\"<SaveName>_<Resolution>_<Bandwidth>\\\"\",\n            zhTW: \"設置保存檔案命名模板, 支持使用變數: \\n\" +\n                  \"<SaveName>, <Id>, <Codecs>, <Language>, <Resolution>, \\n\" +\n                  \"<Bandwidth>, <MediaType>, <Channels>, <FrameRate>, \\n\" +\n                  \"<VideoRange>, <GroupId>, <Ext>\\n\" +\n                  \"示例: --save-pattern \\\"<SaveName>_<Resolution>_<Bandwidth>\\\"\",\n            enUS: \"Set output filename pattern with variables: \\n\" +\n                  \"<SaveName>, <Id>, <Codecs>, <Language>, <Resolution>, \\n\" +\n                  \"<Bandwidth>, <MediaType>, <Channels>, <FrameRate>, \\n\" +\n                  \"<VideoRange>, <GroupId>, <Ext>\\n\" +\n                  \"Example: --save-pattern \\\"<SaveName>_<Resolution>_<Bandwidth>\\\"\"\n        ),\n        [\"cmd_logFilePath\"] = new TextContainer\n        (\n            zhCN: @\"设置日志文件路径, 例如 C:\\Logs\\log.txt\",\n            zhTW: @\"設定日誌檔案路徑, 例如 C:\\Logs\\log.txt\",\n            enUS: @\"Set log file path, Example: C:\\Logs\\log.txt\"\n        ),\n        [\"cmd_skipDownload\"] = new TextContainer\n        (\n            zhCN: \"跳过下载\",\n            zhTW: \"跳過下載\",\n            enUS: \"Skip download\"\n        ),\n        [\"cmd_skipMerge\"] = new TextContainer\n        (\n            zhCN: \"跳过合并分片\",\n            zhTW: \"跳過合併分片\",\n            enUS: \"Skip segments merge\"\n        ),\n        [\"cmd_subFormat\"] = new TextContainer\n        (\n            zhCN: \"字幕输出类型\",\n            zhTW: \"字幕輸出類型\",\n            enUS: \"Subtitle output format\"\n        ),\n        [\"cmd_subOnly\"] = new TextContainer\n        (\n            zhCN: \"只选取字幕轨道\",\n            zhTW: \"只選取字幕軌道\",\n            enUS: \"Select only subtitle tracks\"\n        ),\n        [\"cmd_subtitleFix\"] = new TextContainer\n        (\n            zhCN: \"自动修正字幕\",\n            zhTW: \"自動修正字幕\",\n            enUS: \"Automatically fix subtitles\"\n        ),\n        [\"cmd_threadCount\"] = new TextContainer\n        (\n            zhCN: \"设置下载线程数\",\n            zhTW: \"設置下載執行緒數\",\n            enUS: \"Set download thread count\"\n        ),\n        [\"cmd_tmpDir\"] = new TextContainer\n        (\n            zhCN: \"设置临时文件存储目录\",\n            zhTW: \"設置臨時文件儲存目錄\",\n            enUS: \"Set temporary file directory\"\n        ),\n        [\"cmd_uiLanguage\"] = new TextContainer\n        (\n            zhCN: \"设置UI语言\",\n            zhTW: \"設置UI語言\",\n            enUS: \"Set UI language\"\n        ),\n        [\"cmd_moreHelp\"] = new TextContainer\n        (\n            zhCN: \"查看某个选项的详细帮助信息\",\n            zhTW: \"查看某個選項的詳細幫助訊息\",\n            enUS: \"Set more help info about one option\"\n        ),\n        [\"cmd_urlProcessorArgs\"] = new TextContainer\n        (\n            zhCN: \"此字符串将直接传递给URL Processor\",\n            zhTW: \"此字符串將直接傳遞給URL Processor\",\n            enUS: \"Give these arguments to the URL Processors.\"\n        ),\n        [\"cmd_liveRealTimeMerge\"] = new TextContainer\n        (\n            zhCN: \"录制直播时实时合并\",\n            zhTW: \"錄製直播時即時合併\",\n            enUS: \"Real-time merge into file when recording live\"\n        ),\n        [\"cmd_customProxy\"] = new TextContainer\n        (\n            zhCN: \"设置请求代理, 如 http://127.0.0.1:8888\",\n            zhTW: \"設置請求代理, 如 http://127.0.0.1:8888\",\n            enUS: \"Set web request proxy, like http://127.0.0.1:8888\"\n        ),\n        [\"cmd_customRange\"] = new TextContainer\n        (\n            zhCN: \"仅下载部分分片. 输入 \\\"--morehelp custom-range\\\" 以查看详细信息\",\n            zhTW: \"僅下載部分分片. 輸入 \\\"--morehelp custom-range\\\" 以查看詳細訊息\",\n            enUS: \"Download only part of the segments. Use \\\"--morehelp custom-range\\\" for more details\"\n        ),\n        [\"cmd_useSystemProxy\"] = new TextContainer\n        (\n            zhCN: \"使用系统默认代理\",\n            zhTW: \"使用系統默認代理\",\n            enUS: \"Use system default proxy\"\n        ),\n        [\"cmd_livePerformAsVod\"] = new TextContainer\n        (\n            zhCN: \"以点播方式下载直播流\",\n            zhTW: \"以點播方式下載直播流\",\n            enUS: \"Download live streams as vod\"\n        ),\n        [\"cmd_liveWaitTime\"] = new TextContainer\n        (\n            zhCN: \"手动设置直播列表刷新间隔\",\n            zhTW: \"手動設置直播列表刷新間隔\",\n            enUS: \"Manually set the live playlist refresh interval\"\n        ),\n        [\"cmd_adKeyword\"] = new TextContainer\n        (\n            zhCN: \"设置广告分片的URL关键字(正则表达式)\",\n            zhTW: \"設置廣告分片的URL關鍵字(正則表達式)\",\n            enUS: \"Set URL keywords (regular expressions) for AD segments\"\n        ),\n        [\"cmd_liveTakeCount\"] = new TextContainer\n        (\n            zhCN: \"手动设置录制直播时首次获取分片的数量\",\n            zhTW: \"手動設置錄製直播時首次獲取分片的數量\",\n            enUS: \"Manually set the number of segments downloaded for the first time when recording live\"\n        ),\n        [\"cmd_customHLSMethod\"] = new TextContainer\n        (\n            zhCN: \"指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)\",\n            zhTW: \"指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)\",\n            enUS: \"Set HLS encryption method (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)\"\n        ),\n        [\"cmd_customHLSKey\"] = new TextContainer\n        (\n            zhCN: \"指定HLS解密KEY. 可以是文件, HEX或Base64\",\n            zhTW: \"指定HLS解密KEY. 可以是文件, HEX或Base64\",\n            enUS: \"Set the HLS decryption key. Can be file, HEX or Base64\"\n        ),\n        [\"cmd_customHLSIv\"] = new TextContainer\n        (\n            zhCN: \"指定HLS解密IV. 可以是文件, HEX或Base64\",\n            zhTW: \"指定HLS解密IV. 可以是文件, HEX或Base64\",\n            enUS: \"Set the HLS decryption iv. Can be file, HEX or Base64\"\n        ),\n        [\"cmd_livePipeMux\"] = new TextContainer\n        (\n            zhCN: \"录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件\",\n            zhTW: \"錄製直播並開啟即時合併時通過管道+ffmpeg即時混流到TS文件\",\n            enUS: \"Real-time muxing to TS file through pipeline + ffmpeg (liveRealTimeMerge enabled)\"\n        ),\n        [\"cmd_liveKeepSegments\"] = new TextContainer\n        (\n            zhCN: \"录制直播并开启实时合并时依然保留分片\",\n            zhTW: \"錄製直播並開啟即時合併時依然保留分片\",\n            enUS: \"Keep segments when recording a live (liveRealTimeMerge enabled)\"\n        ),\n        [\"cmd_liveRecordLimit\"] = new TextContainer\n        (\n            zhCN: \"录制直播时的录制时长限制\",\n            zhTW: \"錄製直播時的錄製時長限制\",\n            enUS: \"Recording time limit when recording live\"\n        ),\n        [\"cmd_taskStartAt\"] = new TextContainer\n        (\n            zhCN: \"在此时间之前不会开始执行任务\",\n            zhTW: \"在此時間之前不會開始執行任務\",\n            enUS: \"Task execution will not start before this time\"\n        ),\n        [\"cmd_useShakaPackager\"] = new TextContainer\n        (\n            zhCN: \"解密时使用shaka-packager替代mp4decrypt\",\n            zhTW: \"解密時使用shaka-packager替代mp4decrypt\",\n            enUS: \"Use shaka-packager instead of mp4decrypt to decrypt\"\n        ),\n        [\"cmd_decryptionEngine\"] = new TextContainer\n        (\n            zhCN: \"设置解密时使用的第三方程序\",\n            zhTW: \"設置解密時使用的第三方程序\",\n            enUS: \"Set the third-party program used for decryption\"\n        ),\n        [\"cmd_concurrentDownload\"] = new TextContainer\n        (\n            zhCN: \"并发下载已选择的音频、视频和字幕\",\n            zhTW: \"並發下載已選擇的音訊、影片和字幕\",\n            enUS: \"Concurrently download the selected audio, video and subtitles\"\n        ),\n        [\"cmd_selectVideo\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式选择符合要求的视频流. 输入 \\\"--morehelp select-video\\\" 以查看详细信息\",\n            zhTW: \"通過正則表達式選擇符合要求的影片軌. 輸入 \\\"--morehelp select-video\\\" 以查看詳細訊息\",\n            enUS: \"Select video streams by regular expressions. Use \\\"--morehelp select-video\\\" for more details\"\n        ),\n        [\"cmd_dropVideo\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式去除符合要求的视频流.\",\n            zhTW: \"通過正則表達式去除符合要求的影片串流.\",\n            enUS: \"Drop video streams by regular expressions.\"\n        ),\n        [\"cmd_selectVideo_more\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式选择符合要求的视频流. 你能够以:分隔形式指定如下参数:\\r\\n\\r\\n\" +\n                  \"id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\\r\\n\" +\n                  \"segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\\r\\n\" +\n                  \"plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\\r\\n\\r\\n\" +\n                  \"* for=FOR: 选择方式. best[number], worst[number], all (默认: best)\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 选择最佳视频\\r\\n\" +\n                  \"-sv best\\r\\n\" +\n                  \"# 选择4K+HEVC视频\\r\\n\" +\n                  \"-sv res=\\\"3840*\\\":codecs=hvc1:for=best\\r\\n\" +\n                  \"# 选择长度大于1小时20分钟30秒的视频\\r\\n\" +\n                  \"-sv plistDurMin=\\\"1h20m30s\\\":for=best\\r\\n\" +\n                  \"-sv role=\\\"main\\\":for=best\\r\\n\" +\n                  \"# 选择码率在800Kbps至1Mbps之间的视频\\r\\n\" +\n                  \"-sv bwMin=800:bwMax=1000\\r\\n\",\n            zhTW: \"通過正則表達式選擇符合要求的影片軌. 你能夠以:分隔形式指定如下參數:\\r\\n\\r\\n\" +\n                  \"id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\\r\\n\" +\n                  \"segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\\r\\n\" +\n                  \"plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\\r\\n\\r\\n\" +\n                  \"* for=FOR: 選擇方式. best[number], worst[number], all (默認: best)\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 選擇最佳影片\\r\\n\" +\n                  \"-sv best\\r\\n\" +\n                  \"# 選擇4K+HEVC影片\\r\\n\" +\n                  \"-sv res=\\\"3840*\\\":codecs=hvc1:for=best\\r\\n\" +\n                  \"# 選擇長度大於1小時20分鐘30秒的影片\\r\\n\" +\n                  \"-sv plistDurMin=\\\"1h20m30s\\\":for=best\\r\\n\" +\n                  \"-sv role=\\\"main\\\":for=best\\r\\n\" +\n                  \"# 選擇碼率在800Kbps至1Mbps之間的影片\\r\\n\" +\n                  \"-sv bwMin=800:bwMax=1000\\r\\n\",\n            enUS: \"Select video streams by regular expressions. OPTIONS is a colon separated list of:\\r\\n\\r\\n\" +\n                  \"id=REGEX:lang=REGEX:name=REGEX:codecs=REGEX:res=REGEX:frame=REGEX\\r\\n\" +\n                  \"segsMin=number:segsMax=number:ch=REGEX:range=REGEX:url=REGEX\\r\\n\" +\n                  \"plistDurMin=hms:plistDurMax=hms:bwMin=int:bwMax=int:role=string:for=FOR\\r\\n\\r\\n\" +\n                  \"* for=FOR: Select type. best[number], worst[number], all (Default: best)\\r\\n\\r\\n\" +\n                  \"Examples: \\r\\n\" +\n                  \"# select best video\\r\\n\" +\n                  \"-sv best\\r\\n\" +\n                  \"# select 4K+HEVC video\\r\\n\" +\n                  \"-sv res=\\\"3840*\\\":codecs=hvc1:for=best\\r\\n\" +\n                  \"# Select best video with duration longer than 1 hour 20 minutes 30 seconds\\r\\n\" +\n                  \"-sv plistDurMin=\\\"1h20m30s\\\":for=best\\r\\n\" +\n                  \"-sv role=\\\"main\\\":for=best\\r\\n\" +\n                  \"# Select video with bandwidth between 800Kbps and 1Mbps\\r\\n\" +\n                  \"-sv bwMin=800:bwMax=1000\\r\\n\"\n        ),\n        [\"cmd_selectAudio\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式选择符合要求的音频流. 输入 \\\"--morehelp select-audio\\\" 以查看详细信息\",\n            zhTW: \"通過正則表達式選擇符合要求的音軌. 輸入 \\\"--morehelp select-audio\\\" 以查看詳細訊息\",\n            enUS: \"Select audio streams by regular expressions. Use \\\"--morehelp select-audio\\\" for more details\"\n        ),\n        [\"cmd_dropAudio\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式去除符合要求的音频流.\",\n            zhTW: \"通過正則表達式去除符合要求的音軌.\",\n            enUS: \"Drop audio streams by regular expressions.\"\n        ),\n        [\"cmd_selectAudio_more\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式选择符合要求的音频流. 参考 --select-video\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 选择所有音频\\r\\n\" +\n                  \"-sa all\\r\\n\" +\n                  \"# 选择最佳英语音轨\\r\\n\" +\n                  \"-sa lang=en:for=best\\r\\n\" +\n                  \"# 选择最佳的2条英语(或日语)音轨\\r\\n\" +\n                  \"-sa lang=\\\"ja|en\\\":for=best2\\r\\n\" +\n                  \"-sa role=\\\"main\\\":for=best\\r\\n\",\n            zhTW: \"通過正則表達式選擇符合要求的音軌. 參考 --select-video\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 選擇所有音訊\\r\\n\" +\n                  \"-sa all\\r\\n\" +\n                  \"# 選擇最佳英語音軌\\r\\n\" +\n                  \"-sa lang=en:for=best\\r\\n\" +\n                  \"# 選擇最佳的2條英語(或日語)音軌\\r\\n\" +\n                  \"-sa lang=\\\"ja|en\\\":for=best2\\r\\n\" +\n                  \"-sa role=\\\"main\\\":for=best\\r\\n\",\n            enUS: \"Select audio streams by regular expressions. ref --select-video\\r\\n\\r\\n\" +\n                  \"Examples: \\r\\n\" +\n                  \"# select all\\r\\n\" +\n                  \"-sa all\\r\\n\" +\n                  \"# select best eng audio\\r\\n\" +\n                  \"-sa lang=en:for=best\\r\\n\" +\n                  \"# select best 2, and language is ja or en\\r\\n\" +\n                  \"-sa lang=\\\"ja|en\\\":for=best2\\r\\n\" +\n                  \"-sa role=\\\"main\\\":for=best\\r\\n\"\n        ),\n        [\"cmd_selectSubtitle\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式选择符合要求的字幕流. 输入 \\\"--morehelp select-subtitle\\\" 以查看详细信息\",\n            zhTW: \"通過正則表達式選擇符合要求的字幕流. 輸入 \\\"--morehelp select-subtitle\\\" 以查看詳細訊息\",\n            enUS: \"Select subtitle streams by regular expressions. Use \\\"--morehelp select-subtitle\\\" for more details\"\n        ),\n        [\"cmd_dropSubtitle\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式去除符合要求的字幕流.\",\n            zhTW: \"通過正則表達式去除符合要求的字幕流.\",\n            enUS: \"Drop subtitle streams by regular expressions.\"\n        ),\n        [\"cmd_custom_range\"] = new TextContainer\n        (\n            zhCN: \"下载点播内容时, 仅下载部分分片.\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 下载[0,10]共11个分片\\r\\n\" +\n                  \"--custom-range 0-10\\r\\n\" +\n                  \"# 下载从序号10开始的后续分片\\r\\n\" +\n                  \"--custom-range 10-\\r\\n\" +\n                  \"# 下载前100个分片\\r\\n\" +\n                  \"--custom-range -99\\r\\n\" +\n                  \"# 下载第5分钟到20分钟的内容\\r\\n\" +\n                  \"--custom-range 05:00-20:00\\r\\n\",\n            zhTW: \"下載點播內容時, 僅下載部分分片.\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 下載[0,10]共11個分片\\r\\n\" +\n                  \"--custom-range 0-10\\r\\n\" +\n                  \"# 下載從序號10開始的後續分片\\r\\n\" +\n                  \"--custom-range 10-\\r\\n\" +\n                  \"# 下載前100個分片\\r\\n\" +\n                  \"--custom-range -99\\r\\n\" +\n                  \"# 下載第5分鐘到20分鐘的內容\\r\\n\" +\n                  \"--custom-range 05:00-20:00\\r\\n\",\n            enUS: \"Download only part of the segments when downloading vod content.\\r\\n\\r\\n\" +\n                  \"Examples: \\r\\n\" +\n                  \"# Download [0,10], a total of 11 segments\\r\\n\" +\n                  \"--custom-range 0-10\\r\\n\" +\n                  \"# Download subsequent segments starting from index 10\\r\\n\" +\n                  \"--custom-range 10-\\r\\n\" +\n                  \"# Download the first 100 segments\\r\\n\" +\n                  \"--custom-range -99\\r\\n\" +\n                  \"# Download content from the 05:00 to 20:00\\r\\n\" +\n                  \"--custom-range 05:00-20:00\\r\\n\"\n        ),\n        [\"cmd_selectSubtitle_more\"] = new TextContainer\n        (\n            zhCN: \"通过正则表达式选择符合要求的字幕流. 参考 --select-video\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 选择所有字幕\\r\\n\" +\n                  \"-ss all\\r\\n\" +\n                  \"# 选择所有带有\\\"中文\\\"的字幕\\r\\n\" +\n                  \"-ss name=\\\"中文\\\":for=all\\r\\n\",\n            zhTW: \"通過正則表達式選擇符合要求的字幕流. 參考 --select-video\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 選擇所有字幕\\r\\n\" +\n                  \"-ss all\\r\\n\" +\n                  \"# 選擇所有帶有\\\"中文\\\"的字幕\\r\\n\" +\n                  \"-ss name=\\\"中文\\\":for=all\\r\\n\",\n            enUS: \"Select subtitle streams by regular expressions. ref --select-video\\r\\n\\r\\n\" +\n                  \"Examples: \\r\\n\" +\n                  \"# select all subs\\r\\n\" +\n                  \"-ss all\\r\\n\" +\n                  \"# select all subs containing \\\"English\\\"\\r\\n\" +\n                  \"-ss name=\\\"English\\\":for=all\\r\\n\"\n        ),\n        [\"cmd_muxAfterDone_more\"] = new TextContainer\n        (\n            zhCN: \"所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\\r\\n\\r\\n\" +\n                  \"* format=FORMAT: 指定混流容器 mkv, mp4, ts\\r\\n\" +\n                  \"* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\\r\\n\" +\n                  \"* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\\r\\n\" +\n                  \"* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\\r\\n\" +\n                  \"* keep=BOOL: 混流完成是否保留文件 true, false (默认: false)\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 混流为mp4容器\\r\\n\" +\n                  \"-M format=mp4\\r\\n\" +\n                  \"# 使用mkvmerge, 自动寻找程序\\r\\n\" +\n                  \"-M format=mkv:muxer=mkvmerge\\r\\n\" +\n                  \"# 使用mkvmerge, 自定义程序路径\\r\\n\" +\n                  \"-M format=mkv:muxer=mkvmerge:bin_path=\\\"C\\\\:\\\\Program Files\\\\MKVToolNix\\\\mkvmerge.exe\\\"\\r\\n\",\n            zhTW: \"所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\\r\\n\\r\\n\" +\n                  \"* format=FORMAT: 指定混流容器 mkv, mp4, ts\\r\\n\" +\n                  \"* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默認: ffmpeg)\\r\\n\" +\n                  \"* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\\r\\n\" +\n                  \"* skip_sub=BOOL: 是否忽略字幕文件 (默認: false)\\r\\n\" +\n                  \"* keep=BOOL: 混流完成是否保留文件 true, false (默認: false)\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 混流為mp4容器\\r\\n\" +\n                  \"-M format=mp4\\r\\n\" +\n                  \"# 使用mkvmerge, 自動尋找程序\\r\\n\" +\n                  \"-M format=mkv:muxer=mkvmerge\\r\\n\" +\n                  \"# 使用mkvmerge, 自訂程序路徑\\r\\n\" +\n                  \"-M format=mkv:muxer=mkvmerge:bin_path=\\\"C\\\\:\\\\Program Files\\\\MKVToolNix\\\\mkvmerge.exe\\\"\\r\\n\",\n            enUS: \"When all works is done, try to mux the downloaded streams. OPTIONS is a colon separated list of:\\r\\n\\r\\n\" +\n                  \"* format=FORMAT: set container. mkv, mp4, ts\\r\\n\" +\n                  \"* muxer=MUXER: set muxer. ffmpeg, mkvmerge (Default: ffmpeg)\\r\\n\" +\n                  \"* bin_path=PATH: set binary file path. (Default: auto)\\r\\n\" +\n                  \"* skip_sub=BOOL: set whether or not skip subtitle files (Default: false)\\r\\n\" +\n                  \"* keep=BOOL: set whether or not keep files. true, false (Default: false)\\r\\n\\r\\n\" +\n                  \"Examples: \\r\\n\" +\n                  \"# mux to mp4\\r\\n\" +\n                  \"-M format=mp4\\r\\n\" +\n                  \"# use mkvmerge, auto detect bin path\\r\\n\" +\n                  \"-M format=mkv:muxer=mkvmerge\\r\\n\" +\n                  \"# use mkvmerge, set bin path\\r\\n\" +\n                  \"-M format=mkv:muxer=mkvmerge:bin_path=\\\"C\\\\:\\\\Program Files\\\\MKVToolNix\\\\mkvmerge.exe\\\"\\r\\n\"\n        ),\n        [\"cmd_muxAfterDone\"] = new TextContainer\n        (\n            zhCN: \"所有工作完成时尝试混流分离的音视频. 输入 \\\"--morehelp mux-after-done\\\" 以查看详细信息\",\n            zhTW: \"所有工作完成時嘗試混流分離的影音. 輸入 \\\"--morehelp mux-after-done\\\" 以查看詳細訊息\",\n            enUS: \"When all works is done, try to mux the downloaded streams. Use \\\"--morehelp mux-after-done\\\" for more details\"\n        ),\n        [\"cmd_muxImport\"] = new TextContainer\n        (\n            zhCN: \"混流时引入外部媒体文件. 输入 \\\"--morehelp mux-import\\\" 以查看详细信息\",\n            zhTW: \"混流時引入外部媒體檔案. 輸入 \\\"--morehelp mux-import\\\" 以查看詳細訊息\",\n            enUS: \"When MuxAfterDone enabled, allow to import local media files. Use \\\"--morehelp mux-import\\\" for more details\"\n        ),\n        [\"cmd_muxImport_more\"] = new TextContainer\n        (\n            zhCN: \"混流时引入外部媒体文件. 你能够以:分隔形式指定如下参数:\\r\\n\\r\\n\" +\n                  \"* path=PATH: 指定媒体文件路径\\r\\n\" +\n                  \"* lang=CODE: 指定媒体文件语言代码 (非必须)\\r\\n\" +\n                  \"* name=NAME: 指定媒体文件描述信息 (非必须)\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 引入外部字幕\\r\\n\" +\n                  \"--mux-import path=zh-Hans.srt:lang=chi:name=\\\"中文 (简体)\\\"\\r\\n\" +\n                  \"# 引入外部音轨+字幕\\r\\n\" +\n                  \"--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)\\\"\",\n            zhTW: \"混流時引入外部媒體檔案. 你能夠以:分隔形式指定如下參數:\\r\\n\\r\\n\" +\n                  \"* path=PATH: 指定媒體檔案路徑\\r\\n\" +\n                  \"* lang=CODE: 指定媒體檔案語言代碼 (非必須)\\r\\n\" +\n                  \"* name=NAME: 指定媒體檔案描述訊息 (非必須)\\r\\n\\r\\n\" +\n                  \"例如: \\r\\n\" +\n                  \"# 引入外部字幕\\r\\n\" +\n                  \"--mux-import path=zh-Hant.srt:lang=chi:name=\\\"中文 (繁體)\\\"\\r\\n\" +\n                  \"# 引入外部音軌+字幕\\r\\n\" +\n                  \"--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)\\\"\",\n            enUS: \"When MuxAfterDone enabled, allow to import local media files. OPTIONS is a colon separated list of:\\r\\n\\r\\n\" +\n                  \"* path=PATH: set file path\\r\\n\" +\n                  \"* lang=CODE: set media language code (not required)\\r\\n\" +\n                  \"* name=NAME: set description (not required)\\r\\n\\r\\n\" +\n                  \"Examples: \\r\\n\" +\n                  \"# import subtitle\\r\\n\" +\n                  \"--mux-import path=en-US.srt:lang=eng:name=\\\"English (Original)\\\"\\r\\n\" +\n                  \"# import audio and subtitle\\r\\n\" +\n                  \"--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)\\\"\"\n        ),\n        [\"cmd_writeMetaJson\"] = new TextContainer\n        (\n            zhCN: \"解析后的信息是否输出json文件\",\n            zhTW: \"解析後的訊息是否輸出json文件\",\n            enUS: \"Write meta json after parsed\"\n        ),\n        [\"liveLimit\"] = new TextContainer\n        (\n            zhCN: \"本次直播录制时长上限: \",\n            zhTW: \"本次直播錄製時長上限: \",\n            enUS: \"Live recording duration limit: \"\n        ),\n        [\"realTimeDecMessage\"] = new TextContainer\n        (\n            zhCN: \"启用实时解密时，建议用shaka-packager而非mp4decrypt/ffmpeg\",\n            zhTW: \"啟用即時解密時，建議用shaka-packager而非mp4decrypt/ffmpeg\",\n            enUS: \"When enabling real-time decryption, it is recommended to use shaka-packager instead of mp4decrypt/ffmpeg\"\n        ),\n        [\"liveLimitReached\"] = new TextContainer\n        (\n            zhCN: \"到达直播录制上限，即将停止录制\",\n            zhTW: \"到達直播錄製上限，即將停止錄製\",\n            enUS: \"Live recording limit reached, will stop recording soon\"\n        ),\n        [\"saveName\"] = new TextContainer\n        (\n            zhCN: \"保存文件名: \",\n            zhTW: \"保存檔案名: \",\n            enUS: \"Save Name: \"\n        ),\n        [\"fetch\"] = new TextContainer\n        (\n            zhCN: \"获取: \",\n            zhTW: \"獲取: \",\n            enUS: \"Fetch: \"\n        ),\n        [\"ffmpegMerge\"] = new TextContainer\n        (\n            zhCN: \"调用ffmpeg合并中...\",\n            zhTW: \"調用ffmpeg合併中...\",\n            enUS: \"ffmpeg merging...\"\n        ),\n        [\"ffmpegNotFound\"] = new TextContainer\n        (\n            zhCN: \"找不到ffmpeg，请自行下载：https://ffmpeg.org/download.html\",\n            zhTW: \"找不到ffmpeg，請自行下載：https://ffmpeg.org/download.html\",\n            enUS: \"ffmpeg not found, please download at: https://ffmpeg.org/download.html\"\n        ),\n        [\"mkvmergeNotFound\"] = new TextContainer\n        (\n            zhCN: \"找不到mkvmerge，请自行下载：https://mkvtoolnix.download/downloads.html\",\n            zhTW: \"找不到mkvmerge，請自行下載：https://mkvtoolnix.download/downloads.html\",\n            enUS: \"mkvmerge not found, please download at: https://mkvtoolnix.download/downloads.html\"\n        ),\n        [\"shakaPackagerNotFound\"] = new TextContainer\n        (\n            zhCN: \"找不到shaka-packager，请自行下载：https://github.com/shaka-project/shaka-packager/releases\",\n            zhTW: \"找不到shaka-packager，請自行下載：https://github.com/shaka-project/shaka-packager/releases\",\n            enUS: \"shaka-packager not found, please download at: https://github.com/shaka-project/shaka-packager/releases\"\n        ),\n        [\"mp4decryptNotFound\"] = new TextContainer\n        (\n            zhCN: \"找不到mp4decrypt，请自行下载：https://www.bento4.com/downloads/\",\n            zhTW: \"找不到mp4decrypt，請自行下載：https://www.bento4.com/downloads/\",\n            enUS: \"mp4decrypt not found, please download at: https://www.bento4.com/downloads/\"\n        ),\n        [\"fixingTTML\"] = new TextContainer\n        (\n            zhCN: \"正在提取TTML(raw)字幕...\",\n            zhTW: \"正在提取TTML(raw)字幕...\",\n            enUS: \"Extracting TTML(raw) subtitle...\"\n        ),\n        [\"fixingTTMLmp4\"] = new TextContainer\n        (\n            zhCN: \"正在提取TTML(mp4)字幕...\",\n            zhTW: \"正在提取TTML(mp4)字幕...\",\n            enUS: \"Extracting TTML(mp4) subtitle...\"\n        ),\n        [\"fixingVTT\"] = new TextContainer\n        (\n            zhCN: \"正在提取VTT(raw)字幕...\",\n            zhTW: \"正在提取VTT(raw)字幕...\",\n            enUS: \"Extracting VTT(raw) subtitle...\"\n        ),\n        [\"fixingVTTmp4\"] = new TextContainer\n        (\n            zhCN: \"正在提取VTT(mp4)字幕...\",\n            zhTW: \"正在提取VTT(mp4)字幕...\",\n            enUS: \"Extracting VTT(mp4) subtitle...\"\n        ),\n        [\"keyProcessorNotFound\"] = new TextContainer\n        (\n            zhCN: \"找不到支持的Processor\",\n            zhTW: \"找不到支持的Processor\",\n            enUS: \"No Processor matched\"\n        ),\n        [\"liveFound\"] = new TextContainer\n        (\n            zhCN: \"检测到直播流\",\n            zhTW: \"檢測到直播流\",\n            enUS: \"Live stream found\"\n        ),\n        [\"loadingUrl\"] = new TextContainer\n        (\n            zhCN: \"加载URL: \",\n            zhTW: \"載入URL: \",\n            enUS: \"Loading URL: \"\n        ),\n        [\"masterM3u8Found\"] = new TextContainer\n        (\n            zhCN: \"检测到Master列表，开始解析全部流信息\",\n            zhTW: \"檢測到Master列表，開始解析全部流訊息\",\n            enUS: \"Master List detected, try parse all streams\"\n        ),\n        [\"allowHlsMultiExtMap\"] = new TextContainer\n        (\n            zhCN: \"已经允许识别多个#EXT-X-MAP标签, 本软件可能无法正确处理, 请手动确认内容完整性\",\n            zhTW: \"已經允許識別多個#EXT-X-MAP標籤, 本軟件可能無法正確處理, 請手動確認內容完整性\",\n            enUS: \"Multiple #EXT-X-MAP tags are now allowed for detection. However, this software may not handle them correctly. Please manually verify the content's integrity\"\n        ),\n        [\"matchTS\"] = new TextContainer\n        (\n            zhCN: \"内容匹配: [white on green3]HTTP Live MPEG2-TS[/]\",\n            zhTW: \"內容匹配: [white on green3]HTTP Live MPEG2-TS[/]\",\n            enUS: \"Content Matched: [white on green3]HTTP Live MPEG2-TS[/]\"\n        ),\n        [\"matchDASH\"] = new TextContainer\n        (\n            zhCN: \"内容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]\",\n            zhTW: \"內容匹配: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]\",\n            enUS: \"Content Matched: [white on mediumorchid1]Dynamic Adaptive Streaming over HTTP[/]\"\n        ),\n        [\"matchMSS\"] = new TextContainer\n        (\n            zhCN: \"内容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]\",\n            zhTW: \"內容匹配: [white on steelblue1]Microsoft Smooth Streaming[/]\",\n            enUS: \"Content Matched: [white on steelblue1]Microsoft Smooth Streaming[/]\"\n        ),\n        [\"matchHLS\"] = new TextContainer\n        (\n            zhCN: \"内容匹配: [white on deepskyblue1]HTTP Live Streaming[/]\",\n            zhTW: \"內容匹配: [white on deepskyblue1]HTTP Live Streaming[/]\",\n            enUS: \"Content Matched: [white on deepskyblue1]HTTP Live Streaming[/]\"\n        ),\n        [\"matchBinaryData\"] = new TextContainer\n        (\n            zhCN: \"内容匹配: [white on deepskyblue1]Binary Data[/]\",\n            zhTW: \"內容匹配: [white on deepskyblue1]Binary Data[/]\",\n            enUS: \"Content Matched: [white on deepskyblue1]Binary Data[/]\"\n        ),\n        [\"partMerge\"] = new TextContainer\n        (\n            zhCN: \"分片数量大于1800个，开始分块合并...\",\n            zhTW: \"分片數量大於1800個，開始分塊合併...\",\n            enUS: \"Segments more than 1800, start partial merge...\"\n        ),\n        [\"notSupported\"] = new TextContainer\n        (\n            zhCN: \"当前输入不受支持 \",\n            zhTW: \"當前輸入不受支援 \",\n            enUS: \"Input not supported \"\n        ),\n        [\"parsingStream\"] = new TextContainer\n        (\n            zhCN: \"正在解析媒体信息...\",\n            zhTW: \"正在解析媒體信息...\",\n            enUS: \"Parsing streams...\"\n        ),\n        [\"promptChoiceText\"] = new TextContainer\n        (\n            zhCN: \"[grey](按键盘上下键以浏览更多内容)[/]\",\n            zhTW: \"[grey](按鍵盤上下鍵以瀏覽更多內容)[/]\",\n            enUS: \"[grey](Move up and down to reveal more streams)[/]\"\n        ),\n        [\"promptInfo\"] = new TextContainer\n        (\n            zhCN: \"(按 [blue]空格键[/] 选择流, [green]回车键[/] 完成选择)\",\n            zhTW: \"(按 [blue]空格鍵[/] 選擇流, [green]確認鍵[/] 完成選擇)\",\n            enUS: \"(Press [blue]<space>[/] to toggle a stream, [green]<enter>[/] to accept)\"\n        ),\n        [\"promptTitle\"] = new TextContainer\n        (\n            zhCN: \"请选择 [green]你要下载的内容[/]:\",\n            zhTW: \"請選擇 [green]你要下載的內容[/]:\",\n            enUS: \"Please select [green]what you want to download[/]:\"\n        ),\n        [\"readingInfo\"] = new TextContainer\n        (\n            zhCN: \"读取媒体信息...\",\n            zhTW: \"讀取媒體訊息...\",\n            enUS: \"Reading media info...\"\n        ),\n        [\"searchKey\"] = new TextContainer\n        (\n            zhCN: \"正在尝试从文本文件搜索KEY...\",\n            zhTW: \"正在嘗試從文本文件搜尋KEY...\",\n            enUS: \"Trying to search for KEY from text file...\"\n        ),\n        [\"decryptionFailed\"] = new TextContainer\n        (\n            zhCN: \"解密失败\",\n            zhTW: \"解密失敗\",\n            enUS: \"Decryption failed\"\n        ),\n        [\"segmentCountCheckNotPass\"] = new TextContainer\n        (\n            zhCN: \"分片数量校验不通过, 共{}个,已下载{}.\",\n            zhTW: \"分片數量校驗不通過, 共{}個,已下載{}.\",\n            enUS: \"Segment count check not pass, total: {}, downloaded: {}.\"\n        ),\n        [\"selectedStream\"] = new TextContainer\n        (\n            zhCN: \"已选择的流:\",\n            zhTW: \"已選擇的流:\",\n            enUS: \"Selected streams:\"\n        ),\n        [\"startDownloading\"] = new TextContainer\n        (\n            zhCN: \"开始下载...\",\n            zhTW: \"開始下載...\",\n            enUS: \"Start downloading...\"\n        ),\n        [\"streamsInfo\"] = new TextContainer\n        (\n            zhCN: \"已解析, 共计 {} 条媒体流, 基本流 {} 条, 可选音频流 {} 条, 可选字幕流 {} 条\",\n            zhTW: \"已解析, 共計 {} 條媒體流, 基本流 {} 條, 可選音頻流 {} 條, 可選字幕流 {} 條\",\n            enUS: \"Extracted, there are {} streams, with {} basic streams, {} audio streams, {} subtitle streams\"\n        ),\n        [\"writeJson\"] = new TextContainer\n        (\n            zhCN: \"写出meta json\",\n            zhTW: \"寫出meta json\",\n            enUS: \"Writing meta json\"\n        ),\n        [\"noStreamsToDownload\"] = new TextContainer\n        (\n            zhCN: \"没有找到需要下载的流\",\n            zhTW: \"沒有找到需要下載的流\",\n            enUS: \"No stream found to download\"\n        ),\n        [\"loadUrlFailed\"] = new TextContainer\n        (\n            zhCN: \"加载URL失败\",\n            zhTW: \"載入URL失敗\",\n            enUS: \"Failed to load URL\"\n        ),\n\n    };\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Resource/TextContainer.cs",
    "content": "﻿namespace N_m3u8DL_RE.Common.Resource;\n\ninternal class TextContainer\n{\n    public string ZH_CN { get; }\n    public string ZH_TW { get; }\n    public string EN_US { get; }\n\n    public TextContainer(string zhCN, string zhTW, string enUS)\n    {\n        ZH_CN = zhCN;\n        ZH_TW = zhTW;\n        EN_US = enUS;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/BinaryContentCheckUtil.cs",
    "content": "namespace N_m3u8DL_RE.Common.Util;\n\npublic static class BinaryContentCheckUtil\n{\n    public static bool LooksLikeBinary(ReadOnlySpan<byte> data)\n    {\n        if (data.Length == 0) return false;\n\n        int nonTextCount = 0;\n        int total = 0;\n\n        for (int i = 0; i < data.Length;)\n        {\n            byte b = data[i];\n            total++;\n\n            // NULL 字节 → 几乎可以肯定是二进制\n            if (b == 0x00)\n                return true;\n\n            // 可打印 ASCII\n            if (b is >= 0x20 and <= 0x7E)\n            {\n                i++;\n                continue;\n            }\n\n            // 常见控制符（\\n \\r \\t）\n            if (b is 0x09 or 0x0A or 0x0D)\n            {\n                i++;\n                continue;\n            }\n\n            // UTF-8 多字节字符（包括中文）\n            int seqLen = GetUtf8SequenceLength(b);\n            if (seqLen > 1 && i + seqLen <= data.Length && IsValidUtf8Sequence(data.Slice(i, seqLen)))\n            {\n                i += seqLen;\n                continue;\n            }\n\n            // 其他都算非文本\n            nonTextCount++;\n            i++;\n        }\n\n        // 计算比例：非文本字节超过30% 视为二进制\n        double ratio = (double)nonTextCount / total;\n        return ratio > 0.3;\n    }\n\n    private static int GetUtf8SequenceLength(byte b)\n    {\n        if ((b & 0x80) == 0x00) return 1; // 0xxxxxxx\n        if ((b & 0xE0) == 0xC0) return 2; // 110xxxxx\n        if ((b & 0xF0) == 0xE0) return 3; // 1110xxxx\n        if ((b & 0xF8) == 0xF0) return 4; // 11110xxx\n        return 1;\n    }\n\n    private static bool IsValidUtf8Sequence(ReadOnlySpan<byte> seq)\n    {\n        if (seq.Length <= 1) return false;\n        for (int i = 1; i < seq.Length; i++)\n        {\n            if ((seq[i] & 0xC0) != 0x80)\n                return false;\n        }\n        return true;\n    }\n    \n    public static bool IsMpeg2TsBuffer(ReadOnlySpan<byte> buffer)\n    {\n        const int packetSize = 188;\n        if (buffer.Length < packetSize) return false;\n        int syncCount = 0;\n        for (int i = 0; i < Math.Min(buffer.Length / packetSize, 5); i++)\n        {\n            if (buffer[i * packetSize] == 0x47) syncCount++;\n        }\n        return syncCount >= 3;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/GlobalUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.JsonConverter;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace N_m3u8DL_RE.Common.Util;\n\npublic static class GlobalUtil\n{\n    private static readonly JsonSerializerOptions Options = new()\n    {\n        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,\n        WriteIndented = true,\n        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,\n        Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() }\n    };\n    private static readonly JsonContext Context = new JsonContext(Options);\n\n    public static string ConvertToJson(object o)\n    {\n        if (o is StreamSpec s)\n        {\n            return JsonSerializer.Serialize(s, Context.StreamSpec);\n        }\n        if (o is IOrderedEnumerable<StreamSpec> ss)\n        {\n            return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);\n        }\n        if (o is List<StreamSpec> sList)\n        {\n            return JsonSerializer.Serialize(sList, Context.ListStreamSpec);\n        }\n        if (o is IEnumerable<MediaSegment> mList)\n        {\n            return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment);\n        }\n        return \"{NOT SUPPORTED}\";\n    }\n\n    public static string FormatFileSize(double fileSize)\n    {\n        return fileSize switch\n        {\n            < 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),\n            >= 1024 * 1024 * 1024 => $\"{fileSize / (1024 * 1024 * 1024):########0.00}GB\",\n            >= 1024 * 1024 => $\"{fileSize / (1024 * 1024):####0.00}MB\",\n            >= 1024 => $\"{fileSize / 1024:####0.00}KB\",\n            _ => $\"{fileSize:####0.00}B\"\n        };\n    }\n\n    // 此函数用于格式化输出时长  \n    public static string FormatTime(int time)\n    {\n        TimeSpan ts = new TimeSpan(0, 0, time);\n        string str = \"\";\n        str = (ts.Hours.ToString(\"00\") == \"00\" ? \"\" : ts.Hours.ToString(\"00\") + \"h\") + ts.Minutes.ToString(\"00\") + \"m\" + ts.Seconds.ToString(\"00\") + \"s\";\n        return str;\n    }\n\n    /// <summary>\n    /// 寻找可执行程序\n    /// </summary>\n    /// <param name=\"name\"></param>\n    /// <returns></returns>\n    public static string? FindExecutable(string name)\n    {\n        var fileExt = OperatingSystem.IsWindows() ? \".exe\" : \"\";\n        var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) };\n        var envPath = Environment.GetEnvironmentVariable(\"PATH\")?.Split(Path.PathSeparator) ?? [];\n        return searchPath.Concat(envPath).Select(p => Path.Combine(p!, name + fileExt)).FirstOrDefault(File.Exists);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/HTTPUtil.cs",
    "content": "﻿using System.Net;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\n\nnamespace N_m3u8DL_RE.Common.Util;\n\npublic static class HTTPUtil\n{\n    public static readonly HttpClientHandler HttpClientHandler = new()\n    {\n        AllowAutoRedirect = false,\n        AutomaticDecompression = DecompressionMethods.All,\n        ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true,\n        MaxConnectionsPerServer = 1024,\n    };\n\n    public static readonly HttpClient AppHttpClient = new(HttpClientHandler)\n    {\n        Timeout = TimeSpan.FromSeconds(100),\n        DefaultRequestVersion = HttpVersion.Version20,\n        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,\n    };\n\n    private static async Task<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null)\n    {\n        Logger.Debug(ResString.fetch + url);\n        using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);\n        webRequest.Headers.TryAddWithoutValidation(\"Accept-Encoding\", \"gzip, deflate\");\n        webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse(\"no-cache\");\n        webRequest.Headers.Connection.Clear();\n        if (headers != null)\n        {\n            foreach (var item in headers)\n            {\n                webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value);\n            }\n        }\n\n        Logger.Debug(webRequest.Headers.ToString());\n        // 手动处理跳转，以免自定义Headers丢失\n        var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead);\n        if (((int)webResponse.StatusCode).ToString().StartsWith(\"30\"))\n        {\n            HttpResponseHeaders respHeaders = webResponse.Headers;\n            Logger.Debug(respHeaders.ToString());\n            if (respHeaders.Location != null)\n            {\n                var redirectedUrl = \"\";\n                if (!respHeaders.Location.IsAbsoluteUri)\n                {\n                    Uri uri1 = new Uri(url);\n                    Uri uri2 = new Uri(uri1, respHeaders.Location);\n                    redirectedUrl = uri2.ToString();\n                }\n                else\n                {\n                    redirectedUrl = respHeaders.Location.AbsoluteUri;\n                }\n\n                if (redirectedUrl != url)\n                {\n                    Logger.Extra($\"Redirected => {redirectedUrl}\");\n                    return await DoGetAsync(redirectedUrl, headers);\n                }\n            }\n        }\n\n        // 手动将跳转后的URL设置进去, 用于后续取用\n        webResponse.Headers.Location = new Uri(url);\n        webResponse.EnsureSuccessStatusCode();\n        return webResponse;\n    }\n\n    public static async Task<byte[]> GetBytesAsync(string url, Dictionary<string, string>? headers = null)\n    {\n        if (url.StartsWith(\"file:\"))\n        {\n            return await File.ReadAllBytesAsync(new Uri(url).LocalPath);\n        }\n\n        var webResponse = await DoGetAsync(url, headers);\n        var bytes = await webResponse.Content.ReadAsByteArrayAsync();\n        Logger.Debug(HexUtil.BytesToHex(bytes, \" \"));\n        return bytes;\n    }\n\n    /// <summary>\n    /// 获取网页源码\n    /// </summary>\n    /// <param name=\"url\"></param>\n    /// <param name=\"headers\"></param>\n    /// <returns></returns>\n    public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)\n    {\n        var webResponse = await DoGetAsync(url, headers);\n        string htmlCode = await webResponse.Content.ReadAsStringAsync();\n        Logger.Debug(htmlCode);\n        return htmlCode;\n    }\n\n    /// <summary>\n    /// 获取网页源码和跳转后的URL\n    /// </summary>\n    /// <param name=\"url\"></param>\n    /// <param name=\"headers\"></param>\n    /// <returns>(Source Code, RedirectedUrl)</returns>\n    public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null)\n    {\n        var webResponse = await DoGetAsync(url, headers);\n        var htmlCode = \"\";\n\n        // 如果响应是压缩的（gzip/deflate/br），直接按文本处理\n        var encodings = webResponse.Content.Headers.ContentEncoding;\n        if (encodings.Count != 0)\n        {\n            Logger.Debug($\"Detected compression: {string.Join(\",\", encodings)}\");\n            htmlCode = await webResponse.Content.ReadAsStringAsync();\n            return (htmlCode, webResponse.RequestMessage?.RequestUri?.AbsoluteUri ?? url);\n        }\n\n        // 打开流，读取少量样本检测类型\n        const int sampleSize = 4096;\n        await using var responseStream = await webResponse.Content.ReadAsStreamAsync();\n\n        var buffer = new byte[sampleSize];\n        var bytesRead = await responseStream.ReadAsync(buffer.AsMemory(0, sampleSize));\n\n        // MPEG-TS 检测\n        if (BinaryContentCheckUtil.IsMpeg2TsBuffer(buffer.AsSpan(0, bytesRead)))\n        {\n            Logger.Debug(\"Detected MPEG-TS stream\");\n            return (ResString.ReLiveTs, url);\n        }\n\n        // 启发式判断二进制\n        if (BinaryContentCheckUtil.LooksLikeBinary(buffer.AsSpan(0, bytesRead)))\n        {\n            Logger.Debug(\"Heuristic detection: binary data\");\n            return (ResString.ReBinaryData, url);\n        }\n\n        // 否则是文本，完整读取\n        using var ms = new MemoryStream();\n        ms.Write(buffer, 0, bytesRead);\n        await responseStream.CopyToAsync(ms);\n\n        var allBytes = ms.ToArray();\n        var encoding = GetEncodingFromResponse(webResponse) ?? Encoding.UTF8;\n        htmlCode = encoding.GetString(allBytes);\n\n        return (htmlCode, webResponse.RequestMessage?.RequestUri?.AbsoluteUri ?? url);\n    }\n\n    private static Encoding? GetEncodingFromResponse(HttpResponseMessage response)\n    {\n        var contentType = response.Content.Headers.ContentType;\n        if (contentType?.CharSet == null) return null;\n        \n        try\n        {\n            return Encoding.GetEncoding(contentType.CharSet);\n        }\n        catch (ArgumentException)\n        {\n            // 无效 charset，回退\n        }\n\n        return null;\n    }\n\n    public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)\n    {\n        string htmlCode;\n        using HttpRequestMessage request = new(HttpMethod.Post, Url);\n        request.Headers.TryAddWithoutValidation(\"Content-Type\", \"application/json\");\n        request.Headers.TryAddWithoutValidation(\"Content-Length\", postData.Length.ToString());\n        request.Content = new ByteArrayContent(postData);\n        var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);\n        htmlCode = await webResponse.Content.ReadAsStringAsync();\n        return htmlCode;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/HexUtil.cs",
    "content": "namespace N_m3u8DL_RE.Common.Util;\n\npublic static class HexUtil\n{\n    public static string BytesToHex(byte[] data, string split = \"\")\n    {\n        return BitConverter.ToString(data).Replace(\"-\", split);\n    }\n\n    /// <summary>\n    /// 判断是不是HEX字符串\n    /// </summary>\n    /// <param name=\"input\"></param>\n    /// <returns></returns>\n    public static bool TryParseHexString(string input, out byte[]? bytes)\n    {\n        bytes = null;\n        input = input.ToUpper();\n        if (input.StartsWith(\"0X\"))\n            input = input[2..];\n        if (input.Length % 2 != 0)\n            return false;\n        if (input.Any(c => !\"0123456789ABCDEF\".Contains(c)))\n            return false;\n        bytes = HexToBytes(input);\n        return true;\n    }\n    \n    /// <summary>\n    /// 判断是不是Base64字符串\n    /// </summary>\n    /// <param name=\"s\">input</param>\n    /// <param name=\"key\">hex string</param>\n    /// <returns></returns>\n    public static bool TryParseBase64(string s, out string? key)\n    {\n        key = null;\n        try\n        {\n            key = BytesToHex(Convert.FromBase64String(s));\n            return true;\n        }\n        catch (FormatException)\n        {\n            return false;\n        }\n    }\n\n    public static byte[] HexToBytes(string hex)\n    {\n        var hexSpan = hex.AsSpan().Trim();\n        if (hexSpan.StartsWith(\"0x\") || hexSpan.StartsWith(\"0X\"))\n        {\n            hexSpan = hexSpan[2..];\n        }\n\n        return Convert.FromHexString(hexSpan);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Common/Util/RetryUtil.cs",
    "content": "using System.Net;\nusing N_m3u8DL_RE.Common.Log;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Common.Util;\n\npublic static class RetryUtil\n{\n    public static async Task<T?> WebRequestRetryAsync<T>(Func<Task<T>> funcAsync, int maxRetries = 10, int retryDelayMilliseconds = 1500, int retryDelayIncrementMilliseconds = 0)\n    {\n        var retryCount = 0;\n        var result = default(T);\n        Exception currentException = new();\n\n        while (retryCount < maxRetries)\n        {\n            try\n            {\n                result = await funcAsync();\n                break;\n            }\n            catch (Exception ex) when (ex is WebException or IOException or HttpRequestException)\n            {\n                currentException = ex;\n                retryCount++;\n                Logger.WarnMarkUp($\"[grey]{ex.Message.EscapeMarkup()} ({retryCount}/{maxRetries})[/]\");\n                await Task.Delay(retryDelayMilliseconds + (retryDelayIncrementMilliseconds * (retryCount - 1)));\n            }\n        }\n\n        if (retryCount == maxRetries)\n        {\n            throw new Exception($\"Failed to execute action after {maxRetries} retries.\", currentException);\n        }\n\n        return result;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Processor;\nusing N_m3u8DL_RE.Parser.Processor.DASH;\nusing N_m3u8DL_RE.Parser.Processor.HLS;\n\nnamespace N_m3u8DL_RE.Parser.Config;\n\npublic class ParserConfig\n{\n    public string Url { get; set; } = string.Empty;\n\n    public string OriginalUrl { get; set; } = string.Empty;\n\n    public string BaseUrl { get; set; } = string.Empty;\n    \n    public Dictionary<string, string> CustomParserArgs { get; } = new();\n\n    public Dictionary<string, string> Headers { get; init; } = new();\n\n    /// <summary>\n    /// 内容前置处理器. 调用顺序与列表顺序相同\n    /// </summary>\n    public IList<ContentProcessor> ContentProcessors { get; } = new List<ContentProcessor>() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() };\n\n    /// <summary>\n    /// 添加分片URL前置处理器. 调用顺序与列表顺序相同\n    /// </summary>\n    public IList<UrlProcessor> UrlProcessors { get; } = new List<UrlProcessor>() { new DefaultUrlProcessor() };\n\n    /// <summary>\n    /// KEY解析器. 调用顺序与列表顺序相同\n    /// </summary>\n    public IList<KeyProcessor> KeyProcessors { get; } = new List<KeyProcessor>() { new DefaultHLSKeyProcessor() };\n\n\n    /// <summary>\n    /// 自定义的加密方式\n    /// </summary>\n    public EncryptMethod? CustomMethod { get; set; }\n\n    /// <summary>\n    /// 自定义的解密KEY\n    /// </summary>\n    public byte[]? CustomeKey { get; set; }\n\n    /// <summary>\n    /// 自定义的解密IV\n    /// </summary>\n    public byte[]? CustomeIV { get; set; }\n\n    /// <summary>\n    /// 组装视频分段的URL时，是否要把原本URL后的参数也加上去\n    /// 如 Base URL = \"http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx\"\n    /// 相对路径 = clip_01.ts\n    /// 如果 AppendUrlParams=false，得 http://xxx.com/clip_01.ts\n    /// 如果 AppendUrlParams=true，得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx\n    /// </summary>\n    public bool AppendUrlParams { get; set; } = false;\n\n    /// <summary>\n    /// 此参数将会传递给URL Processor中\n    /// </summary>\n    public string? UrlProcessorArgs { get; set; }\n\n    /// <summary>\n    /// KEY重试次数\n    /// </summary>\n    public int KeyRetryCount { get; set; } = 3;\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Constants/DASHTags.cs",
    "content": "﻿namespace N_m3u8DL_RE.Parser.Constants;\n\ninternal static class DASHTags\n{\n    public const string TemplateRepresentationID = \"$RepresentationID$\";\n    public const string TemplateBandwidth = \"$Bandwidth$\";\n    public const string TemplateNumber = \"$Number$\";\n    public const string TemplateTime = \"$Time$\";\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Constants/HLSTags.cs",
    "content": "﻿namespace N_m3u8DL_RE.Parser.Constants;\n\ninternal static class HLSTags\n{\n    public const string ext_m3u = \"#EXTM3U\";\n    public const string ext_x_targetduration = \"#EXT-X-TARGETDURATION\";\n    public const string ext_x_media_sequence = \"#EXT-X-MEDIA-SEQUENCE\";\n    public const string ext_x_discontinuity_sequence = \"#EXT-X-DISCONTINUITY-SEQUENCE\";\n    public const string ext_x_program_date_time = \"#EXT-X-PROGRAM-DATE-TIME\";\n    public const string ext_x_media = \"#EXT-X-MEDIA\";\n    public const string ext_x_playlist_type = \"#EXT-X-PLAYLIST-TYPE\";\n    public const string ext_x_key = \"#EXT-X-KEY\";\n    public const string ext_x_stream_inf = \"#EXT-X-STREAM-INF\";\n    public const string ext_x_version = \"#EXT-X-VERSION\";\n    public const string ext_x_allow_cache = \"#EXT-X-ALLOW-CACHE\";\n    public const string ext_x_endlist = \"#EXT-X-ENDLIST\";\n    public const string extinf = \"#EXTINF\";\n    public const string ext_i_frames_only = \"#EXT-X-I-FRAMES-ONLY\";\n    public const string ext_x_byterange = \"#EXT-X-BYTERANGE\";\n    public const string ext_x_i_frame_stream_inf = \"#EXT-X-I-FRAME-STREAM-INF\";\n    public const string ext_x_discontinuity = \"#EXT-X-DISCONTINUITY\";\n    public const string ext_x_cue_out_start = \"#EXT-X-CUE-OUT\";\n    public const string ext_x_cue_out = \"#EXT-X-CUE-OUT-CONT\";\n    public const string ext_is_independent_segments = \"#EXT-X-INDEPENDENT-SEGMENTS\";\n    public const string ext_x_scte35 = \"#EXT-OATCLS-SCTE35\";\n    public const string ext_x_cue_start = \"#EXT-X-CUE-OUT\";\n    public const string ext_x_cue_end = \"#EXT-X-CUE-IN\";\n    public const string ext_x_cue_span = \"#EXT-X-CUE-SPAN\";\n    public const string ext_x_map = \"#EXT-X-MAP\";\n    public const string ext_x_start = \"#EXT-X-START\";\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Constants/MSSTags.cs",
    "content": "﻿namespace N_m3u8DL_RE.Parser.Constants;\n\ninternal static class MSSTags\n{\n    public const string Bitrate = \"{Bitrate}\";\n    public const string Bitrate_BK = \"{bitrate}\";\n    public const string StartTime = \"{start_time}\";\n    public const string StartTime_BK = \"{start time}\";\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor2.cs",
    "content": "using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Constants;\nusing N_m3u8DL_RE.Parser.Util;\nusing System.Text.RegularExpressions;\nusing System.Xml;\nusing System.Xml.Linq;\n\nnamespace N_m3u8DL_RE.Parser.Extractor;\n\n// https://blog.csdn.net/leek5533/article/details/117750191\ninternal partial class DASHExtractor2 : IExtractor\n{\n    private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;\n\n    public ExtractorType ExtractorType => ExtractorType.MPEG_DASH;\n\n    private string MpdUrl = string.Empty;\n    private string BaseUrl = string.Empty;\n    private string MpdContent = string.Empty;\n    public ParserConfig ParserConfig { get; set; }\n\n    public DASHExtractor2(ParserConfig parserConfig)\n    {\n        this.ParserConfig = parserConfig;\n        SetInitUrl();\n    }\n\n\n    private void SetInitUrl()\n    {\n        this.MpdUrl = ParserConfig.Url ?? string.Empty;\n        this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.MpdUrl;\n    }\n\n    private string ExtendBaseUrl(XElement element, string oriBaseUrl)\n    {\n        var target = element.Elements().FirstOrDefault(e => e.Name.LocalName == \"BaseURL\");\n        if (target != null)\n        {\n            oriBaseUrl = ParserUtil.CombineURL(oriBaseUrl, target.Value);\n        }\n\n        return oriBaseUrl;\n    }\n\n    private double? GetFrameRate(XElement element)\n    {\n        var frameRate = element.Attribute(\"frameRate\")?.Value;\n        if (frameRate == null || !frameRate.Contains('/')) return null;\n        \n        var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]);\n        frameRate = d.ToString(\"0.000\");\n        return Convert.ToDouble(frameRate);\n    }\n\n    public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)\n    {\n        var streamList = new List<StreamSpec>();\n\n        this.MpdContent = rawText;\n        this.PreProcessContent();\n\n\n        var xmlDocument = XDocument.Parse(MpdContent);\n\n        // 选中第一个MPD节点\n        var mpdElement = xmlDocument.Elements().First(e => e.Name.LocalName == \"MPD\");\n\n        // 类型 static点播, dynamic直播\n        var type = mpdElement.Attribute(\"type\")?.Value;\n        bool isLive = type == \"dynamic\";\n\n        // 分片最大时长\n        var maxSegmentDuration = mpdElement.Attribute(\"maxSegmentDuration\")?.Value;\n        // 分片从该时间起可用\n        var availabilityStartTime = mpdElement.Attribute(\"availabilityStartTime\")?.Value;\n        // 在availabilityStartTime的前XX段时间，分片有效\n        var timeShiftBufferDepth = mpdElement.Attribute(\"timeShiftBufferDepth\")?.Value;\n        if (string.IsNullOrEmpty(timeShiftBufferDepth))\n        {\n            // 如果没有 默认一分钟有效\n            timeShiftBufferDepth = \"PT1M\";\n        }\n        // MPD发布时间\n        var publishTime = mpdElement.Attribute(\"publishTime\")?.Value;\n        // MPD总时长\n        var mediaPresentationDuration = mpdElement.Attribute(\"mediaPresentationDuration\")?.Value;\n\n        // 读取在MPD开头定义的<BaseURL>，并替换本身的URL\n        var baseUrlElement = mpdElement.Elements().FirstOrDefault(e => e.Name.LocalName == \"BaseURL\");\n        if (baseUrlElement != null)\n        {\n            var baseUrl = baseUrlElement.Value;\n            if (baseUrl.Contains(\"kkbox.com.tw/\")) baseUrl = baseUrl.Replace(\"//https:%2F%2F\", \"//\");\n            this.BaseUrl = ParserUtil.CombineURL(this.MpdUrl, baseUrl);\n        }\n\n        // 全部Period\n        var periods = mpdElement.Elements().Where(e => e.Name.LocalName == \"Period\");\n        foreach (var period in periods)\n        {\n            // 本Period时长\n            var periodDuration = period.Attribute(\"duration\")?.Value;\n\n            // 本Period ID\n            var periodId = period.Attribute(\"id\")?.Value;\n\n            // 最终分片会使用的baseurl\n            var segBaseUrl = this.BaseUrl;\n\n            // 处理baseurl嵌套\n            segBaseUrl = ExtendBaseUrl(period, segBaseUrl);\n\n            var adaptationSetsBaseUrl = segBaseUrl;\n\n            // 本Period中的全部AdaptationSet\n            var adaptationSets = period.Elements().Where(e => e.Name.LocalName == \"AdaptationSet\");\n            foreach (var adaptationSet in adaptationSets)\n            {\n                // 处理baseurl嵌套\n                segBaseUrl = ExtendBaseUrl(adaptationSet, segBaseUrl);\n\n                var representationsBaseUrl = segBaseUrl;\n\n                var mimeType = adaptationSet.Attribute(\"contentType\")?.Value ?? adaptationSet.Attribute(\"mimeType\")?.Value;\n                var frameRate = GetFrameRate(adaptationSet);\n                // 本AdaptationSet中的全部Representation\n                var representations = adaptationSet.Elements().Where(e => e.Name.LocalName == \"Representation\");\n                foreach (var representation in representations)\n                {\n                    // 处理baseurl嵌套\n                    segBaseUrl = ExtendBaseUrl(representation, segBaseUrl);\n\n                    if (mimeType == null)\n                    {\n                        mimeType = representation.Attribute(\"contentType\")?.Value ?? representation.Attribute(\"mimeType\")?.Value ?? \"\";\n                    }\n                    var bandwidth = representation.Attribute(\"bandwidth\");\n                    StreamSpec streamSpec = new();\n                    streamSpec.OriginalUrl = ParserConfig.OriginalUrl;\n                    streamSpec.PeriodId = periodId;\n                    streamSpec.Playlist = new Playlist();\n                    streamSpec.Playlist.MediaParts.Add(new MediaPart());\n                    streamSpec.GroupId = representation.Attribute(\"id\")?.Value;\n                    streamSpec.Bandwidth = Convert.ToInt32(bandwidth?.Value ?? \"0\");\n                    streamSpec.Codecs = representation.Attribute(\"codecs\")?.Value ?? adaptationSet.Attribute(\"codecs\")?.Value;\n                    streamSpec.Language = FilterLanguage(representation.Attribute(\"lang\")?.Value ?? adaptationSet.Attribute(\"lang\")?.Value);\n                    streamSpec.FrameRate = frameRate ?? GetFrameRate(representation);\n                    streamSpec.Resolution = representation.Attribute(\"width\")?.Value != null ? $\"{representation.Attribute(\"width\")?.Value}x{representation.Attribute(\"height\")?.Value}\" : null;\n                    streamSpec.Url = MpdUrl;\n                    streamSpec.MediaType = mimeType.Split('/')[0] switch\n                    {\n                        \"text\" => MediaType.SUBTITLES,\n                        \"audio\" => MediaType.AUDIO,\n                        _ => null\n                    };\n                    // 特殊处理\n                    if (representation.Attribute(\"volumeAdjust\") != null)\n                    {\n                        streamSpec.GroupId += \"-\" + representation.Attribute(\"volumeAdjust\")?.Value;\n                    }\n                    // 推测后缀名\n                    var mType = representation.Attribute(\"mimeType\")?.Value ?? adaptationSet.Attribute(\"mimeType\")?.Value;\n                    if (mType != null)\n                    {\n                        var mTypeSplit = mType.Split('/');\n                        streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null;\n                    }\n                    // 优化字幕场景识别\n                    if (streamSpec.Codecs is \"stpp\" or \"wvtt\")\n                    {\n                        streamSpec.MediaType = MediaType.SUBTITLES;\n                    }\n                    // 优化字幕场景识别\n                    var role = representation.Elements().FirstOrDefault(e => e.Name.LocalName == \"Role\") ?? adaptationSet.Elements().FirstOrDefault(e => e.Name.LocalName == \"Role\");\n                    if (role != null)\n                    {\n                        var roleValue = role.Attribute(\"value\")?.Value;\n                        if (Enum.TryParse(roleValue, true, out RoleType roleType))\n                        {\n                            streamSpec.Role = roleType;\n\n                            if (roleType == RoleType.Subtitle)\n                            {\n                                streamSpec.MediaType = MediaType.SUBTITLES;\n                                if (mType != null && mType.Contains(\"ttml\"))\n                                    streamSpec.Extension = \"ttml\";\n                            }\n                        }\n                        else if (roleValue != null && roleValue.Contains('-'))\n                        {\n                            roleValue = roleValue.Replace(\"-\", \"\");\n                            if (Enum.TryParse(roleValue, true, out RoleType roleType_))\n                            {\n                                streamSpec.Role = roleType_;\n\n                                if (roleType_ == RoleType.ForcedSubtitle)\n                                {\n                                    streamSpec.MediaType = MediaType.SUBTITLES; // or maybe MediaType.CLOSED_CAPTIONS?\n                                    if (mType != null && mType.Contains(\"ttml\"))\n                                        streamSpec.Extension = \"ttml\";\n                                }\n                            }\n                        }\n                    }\n                    streamSpec.Playlist.IsLive = isLive;\n                    // 设置刷新间隔 timeShiftBufferDepth / 2\n                    if (timeShiftBufferDepth != null)\n                    {\n                        streamSpec.Playlist.RefreshIntervalMs = XmlConvert.ToTimeSpan(timeShiftBufferDepth).TotalMilliseconds / 2;\n                    }\n\n                    // 读取声道数量\n                    var audioChannelConfiguration = adaptationSet.Elements().Concat(representation.Elements()).FirstOrDefault(e => e.Name.LocalName == \"AudioChannelConfiguration\");\n                    if (audioChannelConfiguration != null)\n                    {\n                        streamSpec.Channels = audioChannelConfiguration.Attribute(\"value\")?.Value;\n                    }\n\n                    // 发布时间\n                    if (!string.IsNullOrEmpty(publishTime))\n                    {\n                        streamSpec.PublishTime = DateTime.Parse(publishTime);\n                    }\n\n\n                    // 第一种形式 SegmentBase\n                    var segmentBaseElement = representation.Elements().FirstOrDefault(e => e.Name.LocalName == \"SegmentBase\");\n                    if (segmentBaseElement != null)\n                    {\n                        // 处理init url\n                        var initialization = segmentBaseElement.Elements().FirstOrDefault(e => e.Name.LocalName == \"Initialization\");\n                        if (initialization != null)\n                        {\n                            var sourceURL = initialization.Attribute(\"sourceURL\")?.Value;\n                            if (sourceURL == null)\n                            {\n                                streamSpec.Playlist.MediaParts[0].MediaSegments.Add\n                                (\n                                    new MediaSegment()\n                                    {\n                                        Index = 0,\n                                        Url = segBaseUrl,\n                                        Duration = XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? \"PT0S\").TotalSeconds\n                                    }\n                                );\n                            }\n                            else\n                            {\n                                var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute(\"sourceURL\")?.Value!);\n                                var initRange = initialization.Attribute(\"range\")?.Value;\n                                streamSpec.Playlist.MediaInit = new MediaSegment();\n                                streamSpec.Playlist.MediaInit.Index = -1; // 便于排序\n                                streamSpec.Playlist.MediaInit.Url = initUrl;\n                                if (initRange != null)\n                                {\n                                    var (start, expect) = ParserUtil.ParseRange(initRange);\n                                    streamSpec.Playlist.MediaInit.StartRange = start;\n                                    streamSpec.Playlist.MediaInit.ExpectLength = expect;\n                                }\n                            }\n                        }\n                    }\n\n                    // 第二种形式 SegmentList.SegmentList\n                    var segmentList = representation.Elements().FirstOrDefault(e => e.Name.LocalName == \"SegmentList\");\n                    if (segmentList != null)\n                    {\n                        var durationStr = segmentList.Attribute(\"duration\")?.Value;\n                        // 处理init url\n                        var initialization = segmentList.Elements().FirstOrDefault(e => e.Name.LocalName == \"Initialization\");\n                        if (initialization != null)\n                        {\n                            var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute(\"sourceURL\")?.Value!);\n                            var initRange = initialization.Attribute(\"range\")?.Value;\n                            streamSpec.Playlist.MediaInit = new MediaSegment();\n                            streamSpec.Playlist.MediaInit.Index = -1; // 便于排序\n                            streamSpec.Playlist.MediaInit.Url = initUrl;\n                            if (initRange != null)\n                            {\n                                var (start, expect) = ParserUtil.ParseRange(initRange);\n                                streamSpec.Playlist.MediaInit.StartRange = start;\n                                streamSpec.Playlist.MediaInit.ExpectLength = expect;\n                            }\n                        }\n                        // 处理分片\n                        var segmentURLs = segmentList.Elements().Where(e => e.Name.LocalName == \"SegmentURL\").ToList();\n                        var timescaleStr = segmentList.Attribute(\"timescale\")?.Value ?? \"1\";\n                        for (int segmentIndex = 0; segmentIndex < segmentURLs.Count; segmentIndex++)\n                        {\n                            var segmentURL = segmentURLs.ElementAt(segmentIndex);\n                            var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute(\"media\")?.Value!);\n                            var mediaRange = segmentURL.Attribute(\"mediaRange\")?.Value;\n                            var timesacle = Convert.ToInt32(timescaleStr);\n                            var duration = Convert.ToInt64(durationStr);\n                            MediaSegment mediaSegment = new();\n                            mediaSegment.Duration = duration / (double)timesacle;\n                            mediaSegment.Url = mediaUrl;\n                            mediaSegment.Index = segmentIndex;\n                            if (mediaRange != null)\n                            {\n                                var (start, expect) = ParserUtil.ParseRange(mediaRange);\n                                mediaSegment.StartRange = start;\n                                mediaSegment.ExpectLength = expect;\n                            }\n                            streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);\n                        }\n                    }\n\n                    // 第三种形式 SegmentTemplate+SegmentTimeline\n                    // 通配符有$RepresentationID$ $Bandwidth$ $Number$ $Time$\n\n                    // adaptationSets中的segmentTemplate\n                    var segmentTemplateElementsOuter = adaptationSet.Elements().Where(e => e.Name.LocalName == \"SegmentTemplate\");\n                    // representation中的segmentTemplate\n                    var segmentTemplateElements = representation.Elements().Where(e => e.Name.LocalName == \"SegmentTemplate\");\n                    if (segmentTemplateElements.Any() || segmentTemplateElementsOuter.Any())\n                    {\n                        // 优先使用最近的元素\n                        var segmentTemplate = (segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault())!;\n                        var segmentTemplateOuter = (segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault())!;\n                        var varDic = new Dictionary<string, object?>();\n                        varDic[DASHTags.TemplateRepresentationID] = streamSpec.GroupId;\n                        varDic[DASHTags.TemplateBandwidth] = bandwidth?.Value;\n                        // presentationTimeOffset\n                        var presentationTimeOffsetStr = segmentTemplate.Attribute(\"presentationTimeOffset\")?.Value ?? segmentTemplateOuter.Attribute(\"presentationTimeOffset\")?.Value ?? \"0\";\n                        // timesacle\n                        var timescaleStr = segmentTemplate.Attribute(\"timescale\")?.Value ?? segmentTemplateOuter.Attribute(\"timescale\")?.Value ?? \"1\";\n                        var durationStr = segmentTemplate.Attribute(\"duration\")?.Value ?? segmentTemplateOuter.Attribute(\"duration\")?.Value;\n                        var startNumberStr = segmentTemplate.Attribute(\"startNumber\")?.Value ?? segmentTemplateOuter.Attribute(\"startNumber\")?.Value ?? \"1\";\n                        // 处理init url\n                        var initialization = segmentTemplate.Attribute(\"initialization\")?.Value ?? segmentTemplateOuter.Attribute(\"initialization\")?.Value;\n                        if (initialization != null)\n                        {\n                            var _init = ParserUtil.ReplaceVars(initialization, varDic);\n                            var initUrl = ParserUtil.CombineURL(segBaseUrl, _init);\n                            streamSpec.Playlist.MediaInit = new MediaSegment();\n                            streamSpec.Playlist.MediaInit.Index = -1; // 便于排序\n                            streamSpec.Playlist.MediaInit.Url = initUrl;\n                        }\n                        // 处理分片\n                        var mediaTemplate = segmentTemplate.Attribute(\"media\")?.Value ?? segmentTemplateOuter.Attribute(\"media\")?.Value;\n                        var segmentTimeline = segmentTemplate.Elements().FirstOrDefault(e => e.Name.LocalName == \"SegmentTimeline\");\n                        if (segmentTimeline != null)\n                        {\n                            // 使用了SegmentTimeline 结果精确\n                            var segNumber = Convert.ToInt64(startNumberStr);\n                            var Ss = segmentTimeline.Elements().Where(e => e.Name.LocalName == \"S\");\n                            var currentTime = 0L;\n                            var segIndex = 0;\n                            foreach (var S in Ss)\n                            {\n                                // 每个S元素包含三个属性:@t(start time)\\@r(repeat count)\\@d(duration)\n                                var _startTimeStr = S.Attribute(\"t\")?.Value;\n                                var _durationStr = S.Attribute(\"d\")?.Value;\n                                var _repeatCountStr = S.Attribute(\"r\")?.Value;\n\n                                if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr);\n                                var _duration = Convert.ToInt64(_durationStr);\n                                var timescale = Convert.ToInt32(timescaleStr);\n                                var _repeatCount = Convert.ToInt64(_repeatCountStr);\n                                varDic[DASHTags.TemplateTime] = currentTime;\n                                varDic[DASHTags.TemplateNumber] = segNumber++;\n                                var hasTime = mediaTemplate!.Contains(DASHTags.TemplateTime);\n                                var media = ParserUtil.ReplaceVars(mediaTemplate!, varDic);\n                                var mediaUrl = ParserUtil.CombineURL(segBaseUrl, media!);\n                                MediaSegment mediaSegment = new();\n                                mediaSegment.Url = mediaUrl;\n                                if (hasTime)\n                                    mediaSegment.NameFromVar = currentTime.ToString();\n                                mediaSegment.Duration = _duration / (double)timescale;\n                                mediaSegment.Index = segIndex++;\n                                streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);\n                                if (_repeatCount < 0)\n                                {\n                                    // 负数表示一直重复 直到period结束 注意减掉已经加入的1个片段\n                                    _repeatCount = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? \"PT0S\").TotalSeconds * timescale / _duration) - 1;\n                                }\n                                for (long i = 0; i < _repeatCount; i++)\n                                {\n                                    currentTime += _duration;\n                                    MediaSegment _mediaSegment = new();\n                                    varDic[DASHTags.TemplateTime] = currentTime;\n                                    varDic[DASHTags.TemplateNumber] = segNumber++;\n                                    var _hashTime = mediaTemplate!.Contains(DASHTags.TemplateTime);\n                                    var _media = ParserUtil.ReplaceVars(mediaTemplate!, varDic);\n                                    var _mediaUrl = ParserUtil.CombineURL(segBaseUrl, _media);\n                                    _mediaSegment.Url = _mediaUrl;\n                                    _mediaSegment.Index = segIndex++;\n                                    _mediaSegment.Duration = _duration / (double)timescale;\n                                    if (_hashTime)\n                                        _mediaSegment.NameFromVar = currentTime.ToString();\n                                    streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment);\n                                }\n                                currentTime += _duration;\n                            }\n                        }\n                        else\n                        {\n                            // 没用SegmentTimeline 需要计算总分片数量 不精确\n                            var timescale = Convert.ToInt32(timescaleStr);\n                            var startNumber = Convert.ToInt64(startNumberStr);\n                            var duration = Convert.ToInt64(durationStr);\n                            var totalNumber = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? \"PT0S\").TotalSeconds * timescale / duration);\n                            // 直播的情况，需要自己计算totalNumber\n                            if (totalNumber == 0 && isLive)\n                            {\n                                var now = DateTime.Now;\n                                var availableTime = DateTime.Parse(availabilityStartTime!);\n                                // 可用时间+偏移量\n                                var offsetMs = TimeSpan.FromMilliseconds(Convert.ToInt64(presentationTimeOffsetStr) / 1000);\n                                availableTime = availableTime.Add(offsetMs);\n                                var ts = now - availableTime;\n                                var updateTs = XmlConvert.ToTimeSpan(timeShiftBufferDepth!);\n                                // (当前时间到发布时间的时间差 - 最小刷新间隔) / 分片时长\n                                startNumber += (long)((ts.TotalSeconds - updateTs.TotalSeconds) * timescale / duration);\n                                totalNumber = (long)(updateTs.TotalSeconds * timescale / duration);\n                            }\n                            for (long index = startNumber, segIndex = 0; index < startNumber + totalNumber; index++, segIndex++)\n                            {\n                                varDic[DASHTags.TemplateNumber] = index;\n                                var hasNumber = mediaTemplate!.Contains(DASHTags.TemplateNumber);\n                                var media = ParserUtil.ReplaceVars(mediaTemplate!, varDic);\n                                var mediaUrl = ParserUtil.CombineURL(segBaseUrl, media!);\n                                MediaSegment mediaSegment = new();\n                                mediaSegment.Url = mediaUrl;\n                                if (hasNumber)\n                                    mediaSegment.NameFromVar = index.ToString();\n                                mediaSegment.Index = isLive ? index : segIndex; // 直播直接用startNumber\n                                mediaSegment.Duration = duration / (double)timescale;\n                                streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);\n                            }\n                        }\n                    }\n\n                    // 如果依旧没被添加分片，直接把BaseUrl塞进去就好\n                    if (streamSpec.Playlist.MediaParts[0].MediaSegments.Count == 0)\n                    {\n                        streamSpec.Playlist.MediaParts[0].MediaSegments.Add\n                        (\n                            new MediaSegment()\n                            {\n                                Index = 0,\n                                Url = segBaseUrl,\n                                Duration = XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? \"PT0S\").TotalSeconds\n                            }\n                        );\n                    }\n\n                    // 判断加密情况\n                    if (adaptationSet.Elements().Concat(representation.Elements()).Any(e => e.Name.LocalName == \"ContentProtection\"))\n                    {\n                        if (streamSpec.Playlist.MediaInit != null)\n                        {\n                            streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD;\n                        }\n                        foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)\n                        {\n                            item.EncryptInfo.Method = DEFAULT_METHOD;\n                        }\n                    }\n\n                    // 处理同一ID分散在不同Period的情况\n                    var _index = streamList.FindIndex(_f => _f.PeriodId != streamSpec.PeriodId && _f.GroupId == streamSpec.GroupId && _f.Resolution == streamSpec.Resolution && _f.MediaType == streamSpec.MediaType);\n                    if (_index > -1)\n                    {\n                        if (isLive)\n                        {\n                            // 直播，这种情况直接略过新的\n                        }\n                        else\n                        {\n                            // 点播，这种情况如果URL不同则作为新的part出现，否则仅把时间加起来\n                            var url1 = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Url;\n                            var url2 = streamSpec.Playlist.MediaParts[0].MediaSegments.LastOrDefault()?.Url;\n                            if (url1 != url2)\n                            {\n                                var startIndex = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Index + 1;\n                                var enumerator = streamSpec.Playlist.MediaParts[0].MediaSegments.GetEnumerator();\n                                while (enumerator.MoveNext())\n                                {\n                                    enumerator.Current.Index += startIndex;\n                                }\n                                streamList[_index].Playlist!.MediaParts.Add(new MediaPart()\n                                {\n                                    MediaSegments = streamSpec.Playlist.MediaParts[0].MediaSegments\n                                });\n                            }\n                            else\n                            {\n                                streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Duration += streamSpec.Playlist.MediaParts[0].MediaSegments.Sum(x => x.Duration);\n                            }\n                        }\n                    }\n                    else\n                    {\n                        // 修复mp4类型字幕\n                        if (streamSpec is { MediaType: MediaType.SUBTITLES, Extension: \"mp4\" })\n                        {\n                            streamSpec.Extension = \"m4s\";\n                        }\n                        // 分片默认后缀m4s\n                        if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == null || streamSpec.Playlist.MediaParts.Sum(x => x.MediaSegments.Count) > 1))\n                        {\n                            streamSpec.Extension = \"m4s\";\n                        }\n                        streamList.Add(streamSpec);\n                    }\n                    // 恢复BaseURL相对位置\n                    segBaseUrl = representationsBaseUrl;\n                }\n                // 恢复BaseURL相对位置\n                segBaseUrl = adaptationSetsBaseUrl;\n            }\n        }\n\n        // 为视频设置默认轨道\n        var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();\n        var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();\n        foreach (var item in streamList.Where(item => !string.IsNullOrEmpty(item.Resolution)))\n        {\n            if (aL.Count != 0)\n            {\n                item.AudioId = aL.OrderByDescending(x => x.Bandwidth).First().GroupId;\n            }\n            if (sL.Count != 0)\n            {\n                item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId;\n            }\n        }\n\n        return Task.FromResult(streamList);\n    }\n\n    /// <summary>\n    /// 如果有非法字符 返回und\n    /// </summary>\n    /// <param name=\"v\"></param>\n    /// <returns></returns>\n    private string? FilterLanguage(string? v)\n    {\n        if (v == null) return null;\n        return LangCodeRegex().IsMatch(v) ? v : \"und\";\n    }\n\n    public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        if (streamSpecs.Count == 0) return;\n\n        var (rawText, url) = (\"\", ParserConfig.Url);\n        try\n        {\n            (rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.Url, ParserConfig.Headers);\n        }\n        catch (HttpRequestException) when (ParserConfig.Url!= ParserConfig.OriginalUrl)\n        {\n            // 当URL无法访问时，再请求原始URL\n            (rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);\n        }\n\n        ParserConfig.Url = url;\n        SetInitUrl();\n\n        var newStreams = await ExtractStreamsAsync(rawText);\n        foreach (var streamSpec in streamSpecs)\n        {\n            // 有的网站每次请求MPD返回的码率不一致，导致ToShortString()无法匹配 无法更新playlist\n            // 故增加通过init url来匹配 (如果有的话)\n            var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());\n            if (!match.Any())\n                match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);\n\n            if (match.Any())\n                streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; // 不更新init\n        }\n        // 这里才调用URL预处理器，节省开销\n        await ProcessUrlAsync(streamSpecs);\n    }\n\n    private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)\n    {\n        foreach (var streamSpec in streamSpecs)\n        {\n            var playlist = streamSpec.Playlist;\n            if (playlist == null) continue;\n            \n            if (playlist.MediaInit != null)\n            {\n                playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);\n            }\n            for (var ii = 0; ii < playlist!.MediaParts.Count; ii++)\n            {\n                var part = playlist.MediaParts[ii];\n                foreach (var mediaSegment in part.MediaSegments)\n                {\n                    mediaSegment.Url = PreProcessUrl(mediaSegment.Url);\n                }\n            }\n        }\n\n        return Task.CompletedTask;\n    }\n\n    public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        // 这里才调用URL预处理器，节省开销\n        await ProcessUrlAsync(streamSpecs);\n    }\n\n    public string PreProcessUrl(string url)\n    {\n        foreach (var p in ParserConfig.UrlProcessors)\n        {\n            if (p.CanProcess(ExtractorType, url, ParserConfig))\n            {\n                url = p.Process(url, ParserConfig);\n            }\n        }\n\n        return url;\n    }\n\n    public void PreProcessContent()\n    {\n        foreach (var p in ParserConfig.ContentProcessors)\n        {\n            if (p.CanProcess(ExtractorType, MpdContent, ParserConfig))\n            {\n                MpdContent = p.Process(MpdContent, ParserConfig);\n            }\n        }\n    }\n\n    [GeneratedRegex(@\"^[\\w_\\-\\d]+$\")]\n    private static partial Regex LangCodeRegex();\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs",
    "content": "﻿using N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Parser.Util;\nusing N_m3u8DL_RE.Parser.Constants;\nusing N_m3u8DL_RE.Common.Util;\n\nnamespace N_m3u8DL_RE.Parser.Extractor;\n\ninternal class HLSExtractor : IExtractor\n{\n    public ExtractorType ExtractorType => ExtractorType.HLS;\n\n    private string M3u8Url = string.Empty;\n    private string BaseUrl = string.Empty;\n    private string M3u8Content = string.Empty;\n    private bool MasterM3u8Flag = false;\n\n    public ParserConfig ParserConfig { get; set; }\n\n    public HLSExtractor(ParserConfig parserConfig)\n    {\n        this.ParserConfig = parserConfig;\n        this.M3u8Url = parserConfig.Url ?? string.Empty;\n        this.SetBaseUrl();\n    }\n\n    private void SetBaseUrl()\n    {\n        this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.M3u8Url;\n    }\n\n    /// <summary>\n    /// 预处理m3u8内容\n    /// </summary>\n    public void PreProcessContent()\n    {\n        M3u8Content = M3u8Content.Trim();\n        if (!M3u8Content.StartsWith(HLSTags.ext_m3u))\n        {\n            throw new Exception(ResString.badM3u8);\n        }\n\n        foreach (var p in ParserConfig.ContentProcessors)\n        {\n            if (p.CanProcess(ExtractorType, M3u8Content, ParserConfig))\n            {\n                M3u8Content = p.Process(M3u8Content, ParserConfig);\n            }\n        }\n    }\n\n    /// <summary>\n    /// 预处理URL\n    /// </summary>\n    public string PreProcessUrl(string url)\n    {\n        foreach (var p in ParserConfig.UrlProcessors)\n        {\n            if (p.CanProcess(ExtractorType, url, ParserConfig))\n            {\n                url = p.Process(url, ParserConfig);\n            }\n        }\n\n        return url;\n    }\n\n    private Task<List<StreamSpec>> ParseMasterListAsync()\n    {\n        MasterM3u8Flag = true;\n\n        List<StreamSpec> streams = [];\n\n        using StringReader sr = new StringReader(M3u8Content);\n        string? line;\n        bool expectPlaylist = false;\n        StreamSpec streamSpec = new();\n\n        while ((line = sr.ReadLine()) != null)\n        {\n            if (string.IsNullOrEmpty(line))\n                continue;\n\n            if (line.StartsWith(HLSTags.ext_x_stream_inf))\n            {\n                streamSpec = new();\n                streamSpec.OriginalUrl = ParserConfig.OriginalUrl;\n                var bandwidth = string.IsNullOrEmpty(ParserUtil.GetAttribute(line, \"AVERAGE-BANDWIDTH\")) ? ParserUtil.GetAttribute(line, \"BANDWIDTH\") : ParserUtil.GetAttribute(line, \"AVERAGE-BANDWIDTH\");\n                streamSpec.Bandwidth = Convert.ToInt32(bandwidth);\n                streamSpec.Codecs = ParserUtil.GetAttribute(line, \"CODECS\");\n                streamSpec.Resolution = ParserUtil.GetAttribute(line, \"RESOLUTION\");\n\n                var frameRate = ParserUtil.GetAttribute(line, \"FRAME-RATE\");\n                if (!string.IsNullOrEmpty(frameRate))\n                    streamSpec.FrameRate = Convert.ToDouble(frameRate);\n\n                var audioId = ParserUtil.GetAttribute(line, \"AUDIO\");\n                if (!string.IsNullOrEmpty(audioId))\n                    streamSpec.AudioId = audioId;\n\n                var videoId = ParserUtil.GetAttribute(line, \"VIDEO\");\n                if (!string.IsNullOrEmpty(videoId))\n                    streamSpec.VideoId = videoId;\n\n                var subtitleId = ParserUtil.GetAttribute(line, \"SUBTITLES\");\n                if (!string.IsNullOrEmpty(subtitleId))\n                    streamSpec.SubtitleId = subtitleId;\n\n                var videoRange = ParserUtil.GetAttribute(line, \"VIDEO-RANGE\");\n                if (!string.IsNullOrEmpty(videoRange))\n                    streamSpec.VideoRange = videoRange;\n\n                // 清除多余的编码信息 dvh1.05.06,ec-3 => dvh1.05.06\n                if (!string.IsNullOrEmpty(streamSpec.Codecs) && !string.IsNullOrEmpty(streamSpec.AudioId))\n                {\n                    streamSpec.Codecs = streamSpec.Codecs.Split(',')[0];\n                }\n\n                expectPlaylist = true;\n            }\n            else if (line.StartsWith(HLSTags.ext_x_media))\n            {\n                streamSpec = new();\n                var type = ParserUtil.GetAttribute(line, \"TYPE\").Replace(\"-\", \"_\");\n                if (Enum.TryParse<MediaType>(type, out var mediaType))\n                {\n                    streamSpec.MediaType = mediaType;\n                }\n\n                // 跳过CLOSED_CAPTIONS类型（目前不支持）\n                if (streamSpec.MediaType == MediaType.CLOSED_CAPTIONS)\n                {\n                    continue;\n                }\n\n                var url = ParserUtil.GetAttribute(line, \"URI\");\n\n                /**\n                 *    The URI attribute of the EXT-X-MEDIA tag is REQUIRED if the media\n                      type is SUBTITLES, but OPTIONAL if the media type is VIDEO or AUDIO.\n                      If the media type is VIDEO or AUDIO, a missing URI attribute\n                      indicates that the media data for this Rendition is included in the\n                      Media Playlist of any EXT-X-STREAM-INF tag referencing this EXT-\n                      X-MEDIA tag.  If the media TYPE is AUDIO and the URI attribute is\n                      missing, clients MUST assume that the audio data for this Rendition\n                      is present in every video Rendition specified by the EXT-X-STREAM-INF\n                      tag.\n\n                      此处直接忽略URI属性为空的情况\n                 */\n                if (string.IsNullOrEmpty(url))\n                {\n                    continue;\n                }\n\n                url = ParserUtil.CombineURL(BaseUrl, url);\n                streamSpec.Url = PreProcessUrl(url);\n\n                var groupId = ParserUtil.GetAttribute(line, \"GROUP-ID\");\n                streamSpec.GroupId = groupId;\n\n                var lang = ParserUtil.GetAttribute(line, \"LANGUAGE\");\n                if (!string.IsNullOrEmpty(lang))\n                    streamSpec.Language = lang;\n\n                var name = ParserUtil.GetAttribute(line, \"NAME\");\n                if (!string.IsNullOrEmpty(name))\n                    streamSpec.Name = name;\n\n                var def = ParserUtil.GetAttribute(line, \"DEFAULT\");\n                if (Enum.TryParse<Choise>(type, out var defaultChoise))\n                {\n                    streamSpec.Default = defaultChoise;\n                }\n\n                var channels = ParserUtil.GetAttribute(line, \"CHANNELS\");\n                if (!string.IsNullOrEmpty(channels))\n                    streamSpec.Channels = channels;\n\n                var characteristics = ParserUtil.GetAttribute(line, \"CHARACTERISTICS\");\n                if (!string.IsNullOrEmpty(characteristics))\n                    streamSpec.Characteristics = characteristics.Split(',').Last().Split('.').Last();\n\n                streams.Add(streamSpec);\n            }\n            else if (line.StartsWith('#'))\n            {\n                continue;\n            }\n            else if (expectPlaylist)\n            {\n                var url = ParserUtil.CombineURL(BaseUrl, line);\n                streamSpec.Url = PreProcessUrl(url);\n                expectPlaylist = false;\n                streams.Add(streamSpec);\n            }\n        }\n\n        return Task.FromResult(streams);\n    }\n\n    private Task<Playlist> ParseListAsync()\n    {\n        // 标记是否已清除广告分片\n        bool hasAd = false;\n        ;\n        bool allowHlsMultiExtMap = ParserConfig.CustomParserArgs.TryGetValue(\"AllowHlsMultiExtMap\", out var allMultiExtMap) && allMultiExtMap == \"true\";\n        if (allowHlsMultiExtMap)\n        {\n            Logger.WarnMarkUp($\"[darkorange3_1]{ResString.allowHlsMultiExtMap}[/]\");\n        }\n        \n        using StringReader sr = new StringReader(M3u8Content);\n        string? line;\n        bool expectSegment = false;\n        bool isEndlist = false;\n        long segIndex = 0;\n        bool isAd = false;\n        long startIndex;\n\n        Playlist playlist = new();\n        List<MediaPart> mediaParts = [];\n\n        // 当前的加密信息\n        EncryptInfo currentEncryptInfo = new();\n        if (ParserConfig.CustomMethod != null)\n            currentEncryptInfo.Method = ParserConfig.CustomMethod.Value;\n        if (ParserConfig.CustomeKey is { Length: > 0 }) \n            currentEncryptInfo.Key = ParserConfig.CustomeKey;\n        if (ParserConfig.CustomeIV is { Length: > 0 })\n            currentEncryptInfo.IV = ParserConfig.CustomeIV;\n        // 上次读取到的加密行，#EXT-X-KEY:……\n        string lastKeyLine = \"\";\n\n        MediaPart mediaPart = new();\n        MediaSegment segment = new();\n        List<MediaSegment> segments = [];\n\n\n        while ((line = sr.ReadLine()) != null)\n        {\n            if (string.IsNullOrEmpty(line))\n                continue;\n\n            // 只下载部分字节\n            if (line.StartsWith(HLSTags.ext_x_byterange))\n            {\n                var p = ParserUtil.GetAttribute(line);\n                var (n, o) = ParserUtil.GetRange(p);\n                segment.ExpectLength = n;\n                segment.StartRange = o ?? segments.Last().StartRange + segments.Last().ExpectLength;\n                expectSegment = true;\n            }\n            else if (line.StartsWith(HLSTags.ext_x_playlist_type))\n            {\n                isEndlist = line.Trim().EndsWith(\"VOD\");\n            }\n            // 国家地理去广告\n            else if (line.StartsWith(\"#UPLYNK-SEGMENT\"))\n            {\n                if (line.Contains(\",ad\"))\n                    isAd = true;\n                else if (line.Contains(\",segment\"))\n                    isAd = false;\n            }\n            // 国家地理去广告\n            else if (isAd)\n            {\n                continue;\n            }\n            // 解析定义的分段长度\n            else if (line.StartsWith(HLSTags.ext_x_targetduration))\n            {\n                playlist.TargetDuration = Convert.ToDouble(ParserUtil.GetAttribute(line));\n            }\n            // 解析起始编号\n            else if (line.StartsWith(HLSTags.ext_x_media_sequence))\n            {\n                segIndex = Convert.ToInt64(ParserUtil.GetAttribute(line));\n                startIndex = segIndex;\n            }\n            // program date time\n            else if (line.StartsWith(HLSTags.ext_x_program_date_time))\n            {\n                segment.DateTime = DateTime.Parse(ParserUtil.GetAttribute(line));\n            }\n            // 解析不连续标记，需要单独合并（timestamp不同）\n            else if (line.StartsWith(HLSTags.ext_x_discontinuity))\n            {\n                // 修复YK去除广告后的遗留问题\n                if (hasAd && mediaParts.Count > 0)\n                {\n                    segments = mediaParts[^1].MediaSegments;\n                    mediaParts.RemoveAt(mediaParts.Count - 1);\n                    hasAd = false;\n                    continue;\n                }\n                // 常规情况的#EXT-X-DISCONTINUITY标记，新建part\n                if (hasAd || segments.Count < 1) continue;\n                \n                mediaParts.Add(new MediaPart\n                {\n                    MediaSegments = segments,\n                });\n                segments = new();\n            }\n            // 解析KEY\n            else if (line.StartsWith(HLSTags.ext_x_key))\n            {\n                // 如果KEY line相同则不再重复解析\n                if (line != lastKeyLine)\n                {\n                    // 调用处理器进行解析\n                    var parsedInfo = ParseKey(line);\n                    currentEncryptInfo.Method = parsedInfo.Method;\n                    currentEncryptInfo.Key = parsedInfo.Key;\n                    currentEncryptInfo.IV = parsedInfo.IV;\n                }\n                lastKeyLine = line;\n            }\n            // 解析分片时长\n            else if (line.StartsWith(HLSTags.extinf))\n            {\n                string[] tmp = ParserUtil.GetAttribute(line).Split(',');\n                segment.Duration = Convert.ToDouble(tmp[0]);\n                segment.Index = segIndex;\n                // 是否有加密，有的话写入KEY和IV\n                if (currentEncryptInfo.Method != EncryptMethod.NONE)\n                {\n                    segment.EncryptInfo.Method = currentEncryptInfo.Method;\n                    segment.EncryptInfo.Key = currentEncryptInfo.Key;\n                    segment.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));\n                }\n                expectSegment = true;\n                segIndex++;\n            }\n            // m3u8主体结束\n            else if (line.StartsWith(HLSTags.ext_x_endlist))\n            {\n                if (segments.Count > 0)\n                {\n                    mediaParts.Add(new MediaPart()\n                    {\n                        MediaSegments = segments\n                    });\n                }\n                segments = new();\n                isEndlist = true;\n            }\n            // #EXT-X-MAP\n            else if (line.StartsWith(HLSTags.ext_x_map))\n            {\n                if (playlist.MediaInit == null || hasAd) \n                {\n                    playlist.MediaInit = new MediaSegment()\n                    {\n                        Url = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, ParserUtil.GetAttribute(line, \"URI\"))),\n                        Index = -1, // 便于排序\n                    };\n                    if (line.Contains(\"BYTERANGE\"))\n                    {\n                        var p = ParserUtil.GetAttribute(line, \"BYTERANGE\");\n                        var (n, o) = ParserUtil.GetRange(p);\n                        playlist.MediaInit.ExpectLength = n;\n                        playlist.MediaInit.StartRange = o ?? 0L;\n                    }\n                    if (currentEncryptInfo.Method == EncryptMethod.NONE) continue;\n                    // 有加密的话写入KEY和IV\n                    playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method;\n                    playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key;\n                    playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));\n                }\n                // 遇到了其他的map，说明已经不是一个视频了，全部丢弃即可\n                else\n                {\n                    if (segments.Count > 0)\n                    {\n                        mediaParts.Add(new MediaPart()\n                        {\n                            MediaSegments = segments\n                        });\n                    }\n                    segments = new();\n                    if (!allowHlsMultiExtMap)\n                    {\n                        isEndlist = true;\n                        break;\n                    }\n                }\n            }\n            // 评论行不解析\n            else if (line.StartsWith('#')) continue;\n            // 空白行不解析\n            else if (line.StartsWith(\"\\r\\n\")) continue;\n            // 解析分片的地址\n            else if (expectSegment)\n            {\n                var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, line));\n                segment.Url = segUrl;\n                segments.Add(segment);\n                segment = new();\n                // YK的广告分段则清除此分片\n                // 需要注意，遇到广告说明程序对上文的#EXT-X-DISCONTINUITY做出的动作是不必要的，\n                // 其实上下文是同一种编码，需要恢复到原先的part上\n                if (segUrl.Contains(\"ccode=\") && segUrl.Contains(\"/ad/\") && segUrl.Contains(\"duration=\"))\n                {\n                    segments.RemoveAt(segments.Count - 1);\n                    segIndex--;\n                    hasAd = true;\n                }\n                // YK广告(4K分辨率测试)\n                if (segUrl.Contains(\"ccode=0902\") && segUrl.Contains(\"duration=\"))\n                {\n                    segments.RemoveAt(segments.Count - 1);\n                    segIndex--;\n                    hasAd = true;\n                }\n                expectSegment = false;\n            }\n        }\n\n        // 直播的情况，无法遇到m3u8结束标记，需要手动将segments加入parts\n        if (!isEndlist)\n        {\n            mediaParts.Add(new MediaPart()\n            {\n                MediaSegments = segments\n            });\n        }\n\n        playlist.MediaParts = mediaParts;\n        playlist.IsLive = !isEndlist;\n\n        // 直播刷新间隔\n        if (playlist.IsLive)\n        {\n            // 由于播放器默认从最后3个分片开始播放 此处设置刷新间隔为TargetDuration的2倍\n            playlist.RefreshIntervalMs = (int)((playlist.TargetDuration ?? 5) * 2 * 1000);\n        }\n\n        return Task.FromResult(playlist);\n    }\n\n    private EncryptInfo ParseKey(string keyLine)\n    {\n        foreach (var p in ParserConfig.KeyProcessors)\n        {\n            if (p.CanProcess(ExtractorType, keyLine, M3u8Url, M3u8Content, ParserConfig))\n            {\n                // 匹配到对应处理器后不再继续\n                return p.Process(keyLine, M3u8Url, M3u8Content, ParserConfig);\n            }\n        }\n\n        throw new Exception(ResString.keyProcessorNotFound);\n    }\n\n    public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)\n    {\n        this.M3u8Content = rawText;\n        this.PreProcessContent();\n        if (M3u8Content.Contains(HLSTags.ext_x_stream_inf))\n        {\n            Logger.Warn(ResString.masterM3u8Found);\n            var lists = await ParseMasterListAsync();\n            lists = lists.DistinctBy(p => p.Url).ToList();\n            return lists;\n        }\n\n        var playlist = await ParseListAsync();\n        return\n        [\n            new()\n            {\n                Url = ParserConfig.Url,\n                Playlist = playlist,\n                Extension = playlist.MediaInit != null ? \"mp4\" : \"ts\"\n            }\n        ];\n    }\n\n    private async Task LoadM3u8FromUrlAsync(string url)\n    {\n        // Logger.Info(ResString.loadingUrl + url);\n        if (url.StartsWith(\"file:\"))\n        {\n            var uri = new Uri(url);\n            this.M3u8Content = File.ReadAllText(uri.LocalPath);\n        }\n        else if (url.StartsWith(\"http\"))\n        {\n            try\n            {\n                (this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, ParserConfig.Headers);\n            }\n            catch (HttpRequestException) when (ParserConfig.OriginalUrl.StartsWith(\"http\") && url != ParserConfig.OriginalUrl)\n            {\n                // 当URL无法访问时，再请求原始URL\n                (this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);\n            }\n        }\n\n        this.M3u8Url = url;\n        this.SetBaseUrl();\n        this.PreProcessContent();\n    }\n\n    /// <summary>\n    /// 从Master链接中刷新各个流的URL\n    /// </summary>\n    /// <param name=\"lists\"></param>\n    /// <returns></returns>\n    private async Task RefreshUrlFromMaster(List<StreamSpec> lists)\n    {\n        // 重新加载master m3u8, 刷新选中流的URL\n        await LoadM3u8FromUrlAsync(ParserConfig.Url);\n        var newStreams = await ParseMasterListAsync();\n        newStreams = newStreams.DistinctBy(p => p.Url).ToList();\n        foreach (var l in lists)\n        {\n            var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()).ToList();\n            if (match.Count == 0) continue;\n            \n            Logger.DebugMarkUp($\"{l.Url} => {match.First().Url}\");\n            l.Url = match.First().Url;\n        }\n    }\n\n    public async Task FetchPlayListAsync(List<StreamSpec> lists)\n    {\n        for (int i = 0; i < lists.Count; i++)\n        {\n            try\n            {\n                // 直接重新加载m3u8\n                await LoadM3u8FromUrlAsync(lists[i].Url!);\n            }\n            catch (HttpRequestException) when (MasterM3u8Flag)\n            {\n                Logger.WarnMarkUp(\"Can not load m3u8. Try refreshing url from master url...\");\n                // 当前URL无法加载 尝试从Master链接中刷新URL\n                await RefreshUrlFromMaster(lists);\n                await LoadM3u8FromUrlAsync(lists[i].Url!);\n            }\n\n            var newPlaylist = await ParseListAsync();\n            if (lists[i].Playlist?.MediaInit != null)\n                lists[i].Playlist!.MediaParts = newPlaylist.MediaParts; // 不更新init\n            else\n                lists[i].Playlist = newPlaylist;\n\n            if (lists[i].MediaType == MediaType.SUBTITLES)\n            {\n                var a = lists[i].Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.Url.Contains(\".ttml\")));\n                var b = lists[i].Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.Url.Contains(\".vtt\") || m.Url.Contains(\".webvtt\")));\n                if (a) lists[i].Extension = \"ttml\";\n                if (b) lists[i].Extension = \"vtt\";\n            }\n            else\n            {\n                lists[i].Extension = lists[i].Playlist!.MediaInit != null ? \"m4s\" : \"ts\";\n            }\n        }\n    }\n\n    public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        await FetchPlayListAsync(streamSpecs);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs",
    "content": "﻿using N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Parser.Extractor;\n\npublic interface IExtractor\n{\n    ExtractorType ExtractorType { get; }\n\n    ParserConfig ParserConfig { get; set; }\n\n    Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);\n\n    Task FetchPlayListAsync(List<StreamSpec> streamSpecs);\n    Task RefreshPlayListAsync(List<StreamSpec> streamSpecs);\n\n    string PreProcessUrl(string url);\n\n    void PreProcessContent();\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/LiveTSExtractor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Parser.Extractor;\n\ninternal class LiveTSExtractor : IExtractor\n{\n    public ExtractorType ExtractorType => ExtractorType.HTTP_LIVE;\n\n    public ParserConfig ParserConfig {get; set;}\n\n    public LiveTSExtractor(ParserConfig parserConfig)\n    {\n        this.ParserConfig = parserConfig;\n    }\n\n    public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)\n    {\n        return Task.FromResult(new List<StreamSpec>\n        {\n            new()\n            {\n                OriginalUrl = ParserConfig.OriginalUrl,\n                Url = ParserConfig.Url,\n                Playlist = new Playlist(),\n                GroupId = ResString.ReLiveTs\n            }\n        });\n    }\n\n    public Task FetchPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        throw new NotImplementedException();\n    }\n\n    public void PreProcessContent()\n    {\n        throw new NotImplementedException();\n    }\n\n    public string PreProcessUrl(string url)\n    {\n        throw new NotImplementedException();\n    }\n\n    public Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        throw new NotImplementedException();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Extractor/MSSExtractor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Constants;\nusing N_m3u8DL_RE.Parser.Mp4;\nusing N_m3u8DL_RE.Parser.Util;\nusing System.Text.RegularExpressions;\nusing System.Xml.Linq;\n\nnamespace N_m3u8DL_RE.Parser.Extractor;\n\n// Microsoft Smooth Streaming\n// https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/manifest\n// file:///C:/Users/nilaoda/Downloads/[MS-SSTR]-180316.pdf\ninternal partial class MSSExtractor : IExtractor\n{\n    [GeneratedRegex(\"00000001\\\\d7([0-9a-fA-F]{6})\")]\n    private static partial Regex VCodecsRegex();\n\n    ////////////////////////////////////////\n        \n    private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;\n\n    public ExtractorType ExtractorType => ExtractorType.MSS;\n\n    private string IsmUrl = string.Empty;\n    private string BaseUrl = string.Empty;\n    private string IsmContent = string.Empty;\n    public ParserConfig ParserConfig { get; set; }\n\n    public MSSExtractor(ParserConfig parserConfig)\n    {\n        this.ParserConfig = parserConfig;\n        SetInitUrl();\n    }\n\n    private void SetInitUrl()\n    {\n        this.IsmUrl = ParserConfig.Url ?? string.Empty;\n        this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.IsmUrl;\n    }\n\n    public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)\n    {\n        var streamList = new List<StreamSpec>();\n        this.IsmContent = rawText;\n        this.PreProcessContent();\n\n        var xmlDocument = XDocument.Parse(IsmContent);\n\n        // 选中第一个SmoothStreamingMedia节点\n        var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == \"SmoothStreamingMedia\");\n        var timeScaleStr = ssmElement.Attribute(\"TimeScale\")?.Value ?? \"10000000\";\n        var durationStr = ssmElement.Attribute(\"Duration\")?.Value;\n        var timescale = Convert.ToInt32(timeScaleStr);\n        var isLiveStr = ssmElement.Attribute(\"IsLive\")?.Value;\n        bool isLive = Convert.ToBoolean(isLiveStr ?? \"FALSE\");\n\n        var isProtection = false;\n        var protectionSystemId = \"\";\n        var protectionData = \"\";\n\n        // 加密检测\n        var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == \"Protection\");\n        if (protectElement != null)\n        {\n            var protectionHeader = protectElement.Element(\"ProtectionHeader\");\n            if (protectionHeader != null)\n            {\n                isProtection = true;\n                protectionSystemId = protectionHeader.Attribute(\"SystemID\")?.Value ?? \"9A04F079-9840-4286-AB92-E65BE0885F95\";\n                protectionData = HexUtil.BytesToHex(Convert.FromBase64String(protectionHeader.Value));\n            }\n        }\n\n        // 所有StreamIndex节点\n        var streamIndexElements = ssmElement.Elements().Where(e => e.Name.LocalName == \"StreamIndex\");\n\n        foreach (var streamIndex in streamIndexElements)\n        {\n            var type = streamIndex.Attribute(\"Type\")?.Value; // \"video\" / \"audio\" / \"text\"\n            var name = streamIndex.Attribute(\"Name\")?.Value;\n            var subType = streamIndex.Attribute(\"Subtype\")?.Value; // text track\n            // 如果有则不从QualityLevel读取\n            // Bitrate = \"{bitrate}\" / \"{Bitrate}\"\n            // StartTimeSubstitution = \"{start time}\" / \"{start_time}\"\n            var urlPattern = streamIndex.Attribute(\"Url\")?.Value;\n            var language = streamIndex.Attribute(\"Language\")?.Value;\n            // 去除不规范的语言标签\n            if (language?.Length != 3) language = null;\n\n            // 所有c节点\n            var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == \"c\");\n\n            // 所有QualityLevel节点\n            var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == \"QualityLevel\");\n\n            foreach (var qualityLevel in qualityLevelElements)\n            {\n                urlPattern = (qualityLevel.Attribute(\"Url\")?.Value ?? urlPattern)!\n                    .Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime);\n                var fourCC = qualityLevel.Attribute(\"FourCC\")!.Value.ToUpper();\n                var samplingRateStr = qualityLevel.Attribute(\"SamplingRate\")?.Value;\n                var bitsPerSampleStr = qualityLevel.Attribute(\"BitsPerSample\")?.Value;\n                var nalUnitLengthFieldStr = qualityLevel.Attribute(\"NALUnitLengthField\")?.Value;\n                var indexStr = qualityLevel.Attribute(\"Index\")?.Value;\n                var codecPrivateData = qualityLevel.Attribute(\"CodecPrivateData\")?.Value ?? \"\";\n                var audioTag = qualityLevel.Attribute(\"AudioTag\")?.Value;\n                var bitrate = Convert.ToInt32(qualityLevel.Attribute(\"Bitrate\")?.Value ?? \"0\");\n                var width = Convert.ToInt32(qualityLevel.Attribute(\"MaxWidth\")?.Value ?? \"0\");\n                var height = Convert.ToInt32(qualityLevel.Attribute(\"MaxHeight\")?.Value ?? \"0\");\n                var channels = qualityLevel.Attribute(\"Channels\")?.Value;\n\n                StreamSpec streamSpec = new();\n                streamSpec.PublishTime = DateTime.Now; // 发布时间默认现在\n                streamSpec.Extension = \"m4s\";\n                streamSpec.OriginalUrl = ParserConfig.OriginalUrl;\n                streamSpec.PeriodId = indexStr;\n                streamSpec.Playlist = new Playlist();\n                streamSpec.Playlist.IsLive = isLive;\n                streamSpec.Playlist.MediaParts.Add(new MediaPart());\n                streamSpec.GroupId = name ?? indexStr;\n                streamSpec.Bandwidth = bitrate;\n                streamSpec.Codecs = ParseCodecs(fourCC, codecPrivateData);\n                streamSpec.Language = language;\n                streamSpec.Resolution = width == 0 ? null : $\"{width}x{height}\";\n                streamSpec.Url = IsmUrl;\n                streamSpec.Channels = channels;\n                streamSpec.MediaType = type switch\n                {\n                    \"text\" => MediaType.SUBTITLES,\n                    \"audio\" => MediaType.AUDIO,\n                    _ => null\n                };\n\n                streamSpec.Playlist.MediaInit = new MediaSegment();\n                if (!string.IsNullOrEmpty(codecPrivateData))\n                {\n                    streamSpec.Playlist.MediaInit.Index = -1; // 便于排序\n                    streamSpec.Playlist.MediaInit.Url = $\"hex://{codecPrivateData}\";\n                }\n\n                var currentTime = 0L;\n                var segIndex = 0;\n                var varDic = new Dictionary<string, object?>();\n                varDic[MSSTags.Bitrate] = bitrate;\n\n                foreach (var c in cElements)\n                {\n                    // 每个C元素包含三个属性:@t(start time)\\@r(repeat count)\\@d(duration)\n                    var _startTimeStr = c.Attribute(\"t\")?.Value;\n                    var _durationStr = c.Attribute(\"d\")?.Value;\n                    var _repeatCountStr = c.Attribute(\"r\")?.Value;\n\n                    if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr);\n                    var _duration = Convert.ToInt64(_durationStr);\n                    var _repeatCount = Convert.ToInt64(_repeatCountStr);\n                    if (_repeatCount > 0)\n                    {\n                        // This value is one-based. (A value of 2 means two fragments in the contiguous series).\n                        _repeatCount -= 1;\n                    }\n\n                    varDic[MSSTags.StartTime] = currentTime;\n                    var oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);\n                    var mediaUrl = ParserUtil.ReplaceVars(oriUrl, varDic);\n                    MediaSegment mediaSegment = new();\n                    mediaSegment.Url = mediaUrl;\n                    if (oriUrl.Contains(MSSTags.StartTime))\n                        mediaSegment.NameFromVar = currentTime.ToString();\n                    mediaSegment.Duration = _duration / (double)timescale;\n                    mediaSegment.Index = segIndex++;\n                    streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);\n                    if (_repeatCount < 0)\n                    {\n                        // 负数表示一直重复 直到period结束 注意减掉已经加入的1个片段\n                        _repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1;\n                    }\n                    for (long i = 0; i < _repeatCount; i++)\n                    {\n                        currentTime += _duration;\n                        MediaSegment _mediaSegment = new();\n                        varDic[MSSTags.StartTime] = currentTime;\n                        var _oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);\n                        var _mediaUrl = ParserUtil.ReplaceVars(_oriUrl, varDic);\n                        _mediaSegment.Url = _mediaUrl;\n                        _mediaSegment.Index = segIndex++;\n                        _mediaSegment.Duration = _duration / (double)timescale;\n                        if (_oriUrl.Contains(MSSTags.StartTime))\n                            _mediaSegment.NameFromVar = currentTime.ToString();\n                        streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment);\n                    }\n                    currentTime += _duration;\n                }\n\n                // 生成MOOV数据\n                if (MSSMoovProcessor.CanHandle(fourCC!))\n                {\n                    streamSpec.MSSData = new MSSData()\n                    {\n                        FourCC = fourCC!,\n                        CodecPrivateData = codecPrivateData,\n                        Type = type!,\n                        Timesacle = Convert.ToInt32(timeScaleStr),\n                        Duration = Convert.ToInt64(durationStr),\n                        SamplingRate = Convert.ToInt32(samplingRateStr ?? \"48000\"),\n                        Channels = Convert.ToInt32(channels ?? \"2\"),\n                        BitsPerSample = Convert.ToInt32(bitsPerSampleStr ?? \"16\"),\n                        NalUnitLengthField = Convert.ToInt32(nalUnitLengthFieldStr ?? \"4\"),\n                        IsProtection = isProtection,\n                        ProtectionData = protectionData,\n                        ProtectionSystemID = protectionSystemId,\n                    };\n                    var processor = new MSSMoovProcessor(streamSpec);\n                    var header = processor.GenHeader(); // trackId可能不正确\n                    streamSpec.Playlist!.MediaInit!.Url = $\"base64://{Convert.ToBase64String(header)}\";\n                    // 为音视频写入加密信息\n                    if (isProtection && type != \"text\") \n                    {\n                        if (streamSpec.Playlist.MediaInit != null)\n                        {\n                            streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD;\n                        }\n                        foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)\n                        {\n                            item.EncryptInfo.Method = DEFAULT_METHOD;\n                        }\n                    }\n                    streamList.Add(streamSpec);\n                }\n                else\n                {\n                    Logger.WarnMarkUp($\"[green]{fourCC}[/] not supported! Skiped.\");\n                }\n            }\n        }\n\n        // 为视频设置默认轨道\n        var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();\n        var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();\n        foreach (var item in streamList.Where(item => !string.IsNullOrEmpty(item.Resolution)))\n        {\n            if (aL.Count != 0)\n            {\n                item.AudioId = aL.First().GroupId;\n            }\n            if (sL.Count != 0)\n            {\n                item.SubtitleId = sL.First().GroupId;\n            }\n        }\n\n        return Task.FromResult(streamList);\n    }\n\n    /// <summary>\n    /// 解析编码\n    /// </summary>\n    /// <param name=\"fourCC\"></param>\n    /// <returns></returns>\n    private static string? ParseCodecs(string fourCC, string? privateData)\n    {\n        if (fourCC == \"TTML\") return \"stpp\";\n        if (string.IsNullOrEmpty(privateData)) return null;\n\n        return fourCC switch\n        {\n            // AVC视频\n            \"H264\" or \"X264\" or \"DAVC\" or \"AVC1\" => ParseAVCCodecs(privateData),\n            // AAC音频\n            \"AAC\" or \"AACL\" or \"AACH\" or \"AACP\" => ParseAACCodecs(fourCC, privateData),\n            // 默认返回fourCC本身\n            _ => fourCC.ToLower()\n        };\n    }\n\n    private static string ParseAVCCodecs(string privateData)\n    {\n        var result = VCodecsRegex().Match(privateData).Groups[1].Value;\n        return string.IsNullOrEmpty(result) ? \"avc1.4D401E\" : $\"avc1.{result}\";\n    }\n\n    private static string ParseAACCodecs(string fourCC, string privateData)\n    {\n        var mpProfile = 2;\n        if (fourCC == \"AACH\")\n        {\n            mpProfile = 5; // High Efficiency AAC Profile\n        }\n        else if (!string.IsNullOrEmpty(privateData)) \n        {\n            mpProfile = (Convert.ToByte(privateData[..2], 16) & 0xF8) >> 3;\n        }\n\n        return $\"mp4a.40.{mpProfile}\";\n    }\n\n    public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        // 这里才调用URL预处理器，节省开销\n        await ProcessUrlAsync(streamSpecs);\n    }\n\n    private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)\n    {\n        foreach (var streamSpec in streamSpecs)\n        {\n            var playlist = streamSpec.Playlist;\n            if (playlist == null) continue;\n            \n            if (playlist.MediaInit != null)\n            {\n                playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);\n            }\n            for (var ii = 0; ii < playlist!.MediaParts.Count; ii++)\n            {\n                var part = playlist.MediaParts[ii];\n                foreach (var segment in part.MediaSegments)\n                {\n                    segment.Url = PreProcessUrl(segment.Url);\n                }\n            }\n        }\n\n        return Task.CompletedTask;\n    }\n\n    public string PreProcessUrl(string url)\n    {\n        foreach (var p in ParserConfig.UrlProcessors)\n        {\n            if (p.CanProcess(ExtractorType, url, ParserConfig))\n            {\n                url = p.Process(url, ParserConfig);\n            }\n        }\n\n        return url;\n    }\n\n    public void PreProcessContent()\n    {\n        foreach (var p in ParserConfig.ContentProcessors)\n        {\n            if (p.CanProcess(ExtractorType, IsmContent, ParserConfig))\n            {\n                IsmContent = p.Process(IsmContent, ParserConfig);\n            }\n        }\n    }\n\n    public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        if (streamSpecs.Count == 0) return;\n\n        var (rawText, url) = (\"\", ParserConfig.Url);\n        try\n        {\n            (rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.Url, ParserConfig.Headers);\n        }\n        catch (HttpRequestException) when (ParserConfig.Url != ParserConfig.OriginalUrl)\n        {\n            // 当URL无法访问时，再请求原始URL\n            (rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);\n        }\n\n        ParserConfig.Url = url;\n        SetInitUrl();\n\n        var newStreams = await ExtractStreamsAsync(rawText);\n        foreach (var streamSpec in streamSpecs)\n        {\n            // 有的网站每次请求MPD返回的码率不一致，导致ToShortString()无法匹配 无法更新playlist\n            // 故增加通过init url来匹配 (如果有的话)\n            var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());\n            if (!match.Any())\n                match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);\n\n            if (match.Any())\n                streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; // 不更新init\n        }\n        // 这里才调用URL预处理器，节省开销\n        await ProcessUrlAsync(streamSpecs);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/InternalsVisibleTo.cs",
    "content": "using System.Runtime.CompilerServices;\n[assembly: InternalsVisibleTo(\"N_m3u8DL-RE.Tests\")]"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/BinaryReader2.cs",
    "content": "﻿namespace Mp4SubtitleParser;\n\n// make BinaryReader in Big Endian\nclass BinaryReader2 : BinaryReader\n{\n    public BinaryReader2(System.IO.Stream stream) : base(stream) { }\n\n    public bool HasMoreData()\n    {\n        return BaseStream.Position < BaseStream.Length;\n    }\n\n    public long GetLength()\n    {\n        return BaseStream.Length;\n    }\n\n    public long GetPosition()\n    {\n        return BaseStream.Position;\n    }\n\n    public override int ReadInt32()\n    {\n        var data = base.ReadBytes(4);\n        if (BitConverter.IsLittleEndian)\n            Array.Reverse(data);\n        return BitConverter.ToInt32(data, 0);\n    }\n\n    public override short ReadInt16()\n    {\n        var data = base.ReadBytes(2);\n        if (BitConverter.IsLittleEndian)\n            Array.Reverse(data);\n        return BitConverter.ToInt16(data, 0);\n    }\n\n    public override long ReadInt64()\n    {\n        var data = base.ReadBytes(8);\n        if (BitConverter.IsLittleEndian)\n            Array.Reverse(data);\n        return BitConverter.ToInt64(data, 0);\n    }\n\n    public override uint ReadUInt32()\n    {\n        var data = base.ReadBytes(4);\n        if (BitConverter.IsLittleEndian)\n            Array.Reverse(data);\n        return BitConverter.ToUInt32(data, 0);\n    }\n\n    public override ulong ReadUInt64()\n    {\n        var data = base.ReadBytes(8);\n        if (BitConverter.IsLittleEndian)\n            Array.Reverse(data);\n        return BitConverter.ToUInt64(data, 0);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/BinaryWriter2.cs",
    "content": "﻿using System.Text;\n\nnamespace Mp4SubtitleParser;\n\n// make BinaryWriter in Big Endian\nclass BinaryWriter2 : BinaryWriter\n{\n    private static bool IsLittleEndian = BitConverter.IsLittleEndian;\n    public BinaryWriter2(System.IO.Stream stream) : base(stream) { }\n\n\n    public void WriteUInt(decimal n, int offset = 0)\n    {\n        var arr = BitConverter.GetBytes((uint)n);\n        if (IsLittleEndian)\n            Array.Reverse(arr);\n        if (offset != 0)\n            arr = arr[offset..];\n        BaseStream.Write(arr);\n    }\n\n    public override void Write(string text)\n    {\n        BaseStream.Write(Encoding.ASCII.GetBytes(text));\n    }\n\n    public void WriteInt(decimal n, int offset = 0)\n    {\n        var arr = BitConverter.GetBytes((int)n);\n        if (IsLittleEndian)\n            Array.Reverse(arr);\n        if (offset != 0)\n            arr = arr[offset..];\n        BaseStream.Write(arr);\n    }\n\n    public void WriteULong(decimal n, int offset = 0)\n    {\n        var arr = BitConverter.GetBytes((ulong)n);\n        if (IsLittleEndian)\n            Array.Reverse(arr);\n        if (offset != 0)\n            arr = arr[offset..];\n        BaseStream.Write(arr);\n    }\n\n    public void WriteUShort(decimal n, int padding = 0)\n    {\n        var arr = BitConverter.GetBytes((ushort)n);\n        if (IsLittleEndian)\n            Array.Reverse(arr);\n        while (padding > 0)\n        {\n            arr = arr.Concat(new byte[] { 0x00 }).ToArray();\n            padding--;\n        }\n        BaseStream.Write(arr);\n    }\n\n    public void WriteShort(decimal n, int padding = 0)\n    {\n        var arr = BitConverter.GetBytes((short)n);\n        if (IsLittleEndian)\n            Array.Reverse(arr);\n        while (padding > 0)\n        {\n            arr = arr.Concat(new byte[] { 0x00 }).ToArray();\n            padding--;\n        }\n        BaseStream.Write(arr);\n    }\n\n    public void WriteByte(byte n, int padding = 0)\n    {\n        var arr = new byte[] { n };\n        while (padding > 0)\n        {\n            arr = arr.Concat(new byte[] { 0x00 }).ToArray();\n            padding--;\n        }\n        BaseStream.Write(arr);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4InitUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Util;\n\nnamespace Mp4SubtitleParser\n{\n    public class ParsedMP4Info\n    {\n        public string? PSSH;\n        public string? KID;\n        public string? Scheme;\n        public bool isMultiDRM;\n    }\n\n    public static class MP4InitUtil\n    {\n        private static readonly byte[] SYSTEM_ID_WIDEVINE = [0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED];\n        private static readonly byte[] SYSTEM_ID_PLAYREADY = [0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95];\n\n        public static ParsedMP4Info ReadInit(byte[] data)\n        {\n            var info = new ParsedMP4Info();\n\n            // parse init\n            new MP4Parser()\n                .Box(\"moov\", MP4Parser.Children)\n                .Box(\"trak\", MP4Parser.Children)\n                .Box(\"mdia\", MP4Parser.Children)\n                .Box(\"minf\", MP4Parser.Children)\n                .Box(\"stbl\", MP4Parser.Children)\n                .FullBox(\"stsd\", MP4Parser.SampleDescription)\n                .FullBox(\"pssh\", box =>\n                {\n                    if (box.Version is not (0 or 1))\n                        throw new Exception(\"PSSH version can only be 0 or 1\");\n                    var systemId = box.Reader.ReadBytes(16);\n                    if (!SYSTEM_ID_WIDEVINE.SequenceEqual(systemId)) return;\n                    \n                    var dataSize = box.Reader.ReadUInt32();\n                    var psshData = box.Reader.ReadBytes((int)dataSize);\n                    info.PSSH = Convert.ToBase64String(psshData);\n                    if (info.KID != \"00000000000000000000000000000000\") return;\n                    \n                    info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower();\n                    info.isMultiDRM = true;\n                })\n                .FullBox(\"encv\", MP4Parser.AllData(data => ReadBox(data, info)))\n                .FullBox(\"enca\", MP4Parser.AllData(data => ReadBox(data, info)))\n                .FullBox(\"enct\", MP4Parser.AllData(data => ReadBox(data, info)))\n                .FullBox(\"encs\", MP4Parser.AllData(data => ReadBox(data, info)))\n                .Parse(data, stopOnPartial: true);\n\n            return info;\n        }\n\n        private static void ReadBox(byte[] data, ParsedMP4Info info)\n        {\n            // find schm \n            byte[] schmBytes = [0x73, 0x63, 0x68, 0x6d];\n            var schmIndex = 0;\n            for (var i = 0; i < data.Length - 4; i++) \n            {\n                if (new[] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(schmBytes))\n                {\n                    schmIndex = i;\n                    break;\n                }\n            }\n            if (schmIndex + 8 < data.Length)\n            {\n                info.Scheme = System.Text.Encoding.UTF8.GetString(data[schmIndex..][8..12]);\n            }\n\n            // if (info.Scheme != \"cenc\") return;\n\n            // find KID\n            byte[] tencBytes = [0x74, 0x65, 0x6E, 0x63];\n            var tencIndex = -1;\n            for (int i = 0; i < data.Length - 4; i++)\n            {\n                if (new[] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(tencBytes))\n                {\n                    tencIndex = i;\n                    break;\n                }\n            }\n            if (tencIndex != -1 && tencIndex + 12 < data.Length) \n            {\n                info.KID = HexUtil.BytesToHex(data[tencIndex..][12..28]).ToLower();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4Parser.cs",
    "content": "﻿using System.Text;\n\n/**\n * Translated from shaka-player project\n * https://github.com/nilaoda/Mp4SubtitleParser\n * https://github.com/shaka-project/shaka-player\n */\nnamespace Mp4SubtitleParser\n{\n    class ParsedBox\n    {\n        public required MP4Parser Parser { get; set; }\n        public bool PartialOkay { get; set; }\n        public long Start { get; set; }\n        public uint Version { get; set; } = 1000;\n        public uint Flags { get; set; } = 1000;\n        public required BinaryReader2 Reader { get; set; }\n        public bool Has64BitSize { get; set; }\n    }\n\n    class TFHD\n    {\n        public uint TrackId { get; set; }\n        public uint DefaultSampleDuration { get; set; }\n        public uint DefaultSampleSize { get; set; }\n    }\n\n    class TRUN\n    {\n        public uint SampleCount { get; set; }\n        public List<Sample> SampleData { get; set; } = [];\n    }\n\n    class Sample\n    {\n        public uint SampleDuration { get; set; }\n        public uint SampleSize { get; set; }\n        public uint SampleCompositionTimeOffset { get; set; }\n    }\n\n    enum BoxType\n    {\n        BASIC_BOX = 0,\n        FULL_BOX = 1\n    };\n\n    class MP4Parser\n    {\n        public bool Done { get; set; } = false;\n        public Dictionary<long, int> Headers { get; set; } = new Dictionary<long, int>();\n        public Dictionary<long, BoxHandler> BoxDefinitions { get; set; } = new Dictionary<long, BoxHandler>();\n\n        public delegate void BoxHandler(ParsedBox box);\n        public delegate void DataHandler(byte[] data);\n\n        public static BoxHandler AllData(DataHandler handler)\n        {\n            return box =>\n            {\n                var all = box.Reader.GetLength() - box.Reader.GetPosition();\n                handler(box.Reader.ReadBytes((int)all));\n            };\n        }\n\n        public static void Children(ParsedBox box)\n        {\n            var headerSize = HeaderSize(box);\n            while (box.Reader.HasMoreData() && !box.Parser.Done)\n            {\n                box.Parser.ParseNext(box.Start + headerSize, box.Reader, box.PartialOkay);\n            }\n        }\n\n        public static void SampleDescription(ParsedBox box)\n        {\n            var headerSize = HeaderSize(box);\n            var count = box.Reader.ReadUInt32();\n            for (int i = 0; i < count; i++)\n            {\n                box.Parser.ParseNext(box.Start + headerSize, box.Reader, box.PartialOkay);\n                if (box.Parser.Done)\n                {\n                    break;\n                }\n            }\n        }\n\n        public void Parse(byte[] data, bool partialOkay = false, bool stopOnPartial = false)\n        {\n            var reader = new BinaryReader2(new MemoryStream(data));\n            this.Done = false;\n            while (reader.HasMoreData() && !this.Done) \n            {\n                this.ParseNext(0, reader, partialOkay, stopOnPartial);\n            }\n        }\n\n        private void ParseNext(long absStart, BinaryReader2 reader, bool partialOkay, bool stopOnPartial = false)\n        {\n            var start = reader.GetPosition();\n\n            // size(4 bytes) + type(4 bytes) = 8 bytes\n            if (stopOnPartial && start + 8 > reader.GetLength())\n            {\n                this.Done = true;\n                return;\n            }\n\n            long size = reader.ReadUInt32();\n            long type = reader.ReadUInt32();\n            var name = TypeToString(type);\n            var has64BitSize = false;\n\n            // Console.WriteLine($\"Parsing MP4 box: {name}\");\n\n            switch (size)\n            {\n                case 0:\n                    size = reader.GetLength() - start;\n                    break;\n                case 1:\n                    if (stopOnPartial && reader.GetPosition() + 8 > reader.GetLength())\n                    {\n                        this.Done = true;\n                        return;\n                    }\n                    size = (long)reader.ReadUInt64();\n                    has64BitSize = true;\n                    break;\n            }\n\n            this.BoxDefinitions.TryGetValue(type, out BoxHandler? boxDefinition);\n\n            if (boxDefinition != null)\n            {\n                uint version = 1000;\n                uint flags = 1000;\n\n                if (this.Headers[type] == (int)BoxType.FULL_BOX)\n                {\n                    if (stopOnPartial && reader.GetPosition() + 4 > reader.GetLength())\n                    {\n                        this.Done = true;\n                        return;\n                    }\n                    var versionAndFlags = reader.ReadUInt32();\n                    version = versionAndFlags >> 24;\n                    flags = versionAndFlags & 0xFFFFFF;\n                }\n                var end = start + size;\n                if (partialOkay && end > reader.GetLength())\n                {\n                    // For partial reads, truncate the payload if we must.\n                    end = reader.GetLength();\n                }\n\n                if (stopOnPartial && end > reader.GetLength())\n                {\n                    this.Done = true;\n                    return;\n                }\n\n                int payloadSize = (int)(end - reader.GetPosition());\n                var payload = (payloadSize > 0) ? reader.ReadBytes(payloadSize) : [];\n                var box = new ParsedBox()\n                {\n                    Parser = this,\n                    PartialOkay = partialOkay || false,\n                    Version = version,\n                    Flags = flags,\n                    Reader = new BinaryReader2(new MemoryStream(payload)),\n                    Start = start + absStart,\n                    Has64BitSize = has64BitSize,\n                };\n\n                boxDefinition(box);\n            }\n            else\n            {\n                // Move the read head to be at the end of the box.\n                // If the box is longer than the remaining parts of the file, e.g. the\n                // mp4 is improperly formatted, or this was a partial range request that\n                // ended in the middle of a box, just skip to the end.\n                var skipLength = Math.Min(\n                  start + size - reader.GetPosition(),\n                  reader.GetLength() - reader.GetPosition());\n                reader.ReadBytes((int)skipLength);\n            }\n        }\n\n\n        private static int HeaderSize(ParsedBox box)\n        {\n            return /* basic header */ 8\n                + /* additional 64-bit size field */ (box.Has64BitSize ? 8 : 0)\n                + /* version and flags for a \"full\" box */ (box.Flags != 0 ? 4 : 0);\n        }\n\n        public static string TypeToString(long type)\n        {\n            return Encoding.UTF8.GetString(new byte[]\n            {\n                 (byte)((type >> 24) & 0xff),\n                 (byte)((type >> 16) & 0xff),\n                 (byte)((type >> 8) & 0xff),\n                 (byte)(type & 0xff)\n            });\n        }\n\n        private static int TypeFromString(string name)\n        {\n            if (name.Length != 4)\n                throw new Exception(\"Mp4 box names must be 4 characters long\");\n            var code = 0;\n            foreach (var chr in name) {\n                code = (code << 8) | chr;\n            }\n            return code;\n        }\n\n        public MP4Parser Box(string type, BoxHandler handler)\n        {\n            var typeCode = TypeFromString(type);\n            this.Headers[typeCode] = (int)BoxType.BASIC_BOX;\n            this.BoxDefinitions[typeCode] = handler;\n            return this;\n        }\n\n        public MP4Parser FullBox(string type, BoxHandler handler)\n        {\n            var typeCode = TypeFromString(type);\n            this.Headers[typeCode] = (int)BoxType.FULL_BOX;\n            this.BoxDefinitions[typeCode] = handler;\n            return this;\n        }\n\n        public static uint ParseMDHD(BinaryReader2 reader, uint version)\n        {\n            if (version == 1)\n            {\n                reader.ReadBytes(8); // Skip \"creation_time\"\n                reader.ReadBytes(8); // Skip \"modification_time\"\n            }\n            else\n            {\n                reader.ReadBytes(4); // Skip \"creation_time\"\n                reader.ReadBytes(4); // Skip \"modification_time\"\n            }\n\n            return reader.ReadUInt32();\n        }\n\n        public static ulong ParseTFDT(BinaryReader2 reader, uint version)\n        {\n            return version == 1 ? reader.ReadUInt64() : reader.ReadUInt32();\n        }\n\n        public static TFHD ParseTFHD(BinaryReader2 reader, uint flags)\n        {\n            var trackId = reader.ReadUInt32();\n            uint defaultSampleDuration = 0;\n            uint defaultSampleSize = 0;\n\n            // Skip \"base_data_offset\" if present.\n            if ((flags & 0x000001) != 0) \n            {\n                reader.ReadBytes(8);\n            }\n\n            // Skip \"sample_description_index\" if present.\n            if ((flags & 0x000002) != 0)\n            {\n                reader.ReadBytes(4);\n            }\n\n            // Read \"default_sample_duration\" if present.\n            if ((flags & 0x000008) != 0)\n            {\n                defaultSampleDuration = reader.ReadUInt32();\n            }\n\n            // Read \"default_sample_size\" if present.\n            if ((flags & 0x000010) != 0)\n            {\n                defaultSampleSize = reader.ReadUInt32();\n            }\n\n            return new TFHD() { TrackId = trackId, DefaultSampleDuration = defaultSampleDuration, DefaultSampleSize = defaultSampleSize };\n        }\n\n        public static TRUN ParseTRUN(BinaryReader2 reader, uint version, uint flags)\n        {\n            var trun = new TRUN();\n            trun.SampleCount = reader.ReadUInt32();\n\n            // Skip \"data_offset\" if present.\n            if ((flags & 0x000001) != 0) \n            {\n                reader.ReadBytes(4);\n            }\n\n            // Skip \"first_sample_flags\" if present.\n            if ((flags & 0x000004) != 0)\n            {\n                reader.ReadBytes(4);\n            }\n\n            for (int i = 0; i < trun.SampleCount; i++)\n            {\n                var sample = new Sample();\n\n                // Read \"sample duration\" if present.\n                if ((flags & 0x000100) != 0)\n                {\n                    sample.SampleDuration = reader.ReadUInt32();\n                }\n\n                // Read \"sample_size\" if present.\n                if ((flags & 0x000200) != 0)\n                {\n                    sample.SampleSize = reader.ReadUInt32();\n                }\n\n                // Skip \"sample_flags\" if present.\n                if ((flags & 0x000400) != 0)\n                {\n                    reader.ReadBytes(4);\n                }\n\n                // Read \"sample_time_offset\" if present.\n                if ((flags & 0x000800) != 0)\n                {\n                    sample.SampleCompositionTimeOffset = version == 0 ?\n                          reader.ReadUInt32() :\n                          (uint)reader.ReadInt32();\n                }\n\n                trun.SampleData.Add(sample);\n            }\n\n            return trun;\n        }\n    }\n}\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4TtmlUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Xml;\n\nnamespace Mp4SubtitleParser;\n\nclass SubEntity\n{\n    public required string Begin { get; set; }\n    public required string End { get; set; }\n    public required string Region { get; set; }\n    public List<XmlElement> Contents { get; set; } = [];\n    public List<string> ContentStrings { get; set; } = [];\n\n    public override bool Equals(object? obj)\n    {\n        return obj is SubEntity entity &&\n               Begin == entity.Begin &&\n               End == entity.End &&\n               Region == entity.Region &&\n               ContentStrings.SequenceEqual(entity.ContentStrings);\n    }\n\n    public override int GetHashCode()\n    {\n        return HashCode.Combine(Begin, End, Region, ContentStrings);\n    }\n}\n\npublic static partial class MP4TtmlUtil\n{\n    [GeneratedRegex(\" \\\\w+:\\\\w+=\\\\\\\"[^\\\\\\\"]*\\\\\\\"\")]\n    private static partial Regex AttrRegex();\n    [GeneratedRegex(\"<p.*?>((.|\\n)+?)<\\\\/p>\")]\n    private static partial Regex LabelFixRegex();\n    [GeneratedRegex(@\"\\<tt[\\s\\S]*?\\<\\/tt\\>\")]\n    private static partial Regex MultiElementsFixRegex();\n    [GeneratedRegex(\"\\\\<smpte:image.*xml:id=\\\\\\\"(.*?)\\\\\\\".*\\\\>([\\\\s\\\\S]*?)<\\\\/smpte:image>\")]\n    private static partial Regex ImageRegex();\n\n    public static bool CheckInit(byte[] data)\n    {\n        bool sawSTPP = false;\n\n        // parse init\n        new MP4Parser()\n            .Box(\"moov\", MP4Parser.Children)\n            .Box(\"trak\", MP4Parser.Children)\n            .Box(\"mdia\", MP4Parser.Children)\n            .Box(\"minf\", MP4Parser.Children)\n            .Box(\"stbl\", MP4Parser.Children)\n            .FullBox(\"stsd\", MP4Parser.SampleDescription)\n            .Box(\"stpp\", box => {\n                sawSTPP = true;\n            })\n            .Parse(data);\n\n        return sawSTPP;\n    }\n\n    private static string ShiftTime(string xmlSrc, long segTimeMs, int index)\n    {\n        string Add(string xmlTime)\n        {\n            var dt = DateTime.ParseExact(xmlTime, \"HH:mm:ss.fff\", System.Globalization.CultureInfo.InvariantCulture);\n            var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index);\n            return $\"{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}.{ts.Milliseconds:000}\";\n        }\n\n        if (!xmlSrc.Contains(\"<tt\") || !xmlSrc.Contains(\"<head>\")) return xmlSrc;\n        var xmlDoc = new XmlDocument();\n        XmlNamespaceManager? nsMgr = null;\n        xmlDoc.LoadXml(xmlSrc);\n        var ttNode = xmlDoc.LastChild;\n        if (nsMgr == null)\n        {\n            var ns = ((XmlElement)ttNode!).GetAttribute(\"xmlns\");\n            nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);\n            nsMgr.AddNamespace(\"ns\", ns);\n        }\n\n        var bodyNode = ttNode!.SelectSingleNode(\"ns:body\", nsMgr);\n        if (bodyNode == null)\n            return xmlSrc;\n\n        var _div = bodyNode.SelectSingleNode(\"ns:div\", nsMgr);\n        // Parse <p> label\n        foreach (XmlElement _p in _div!.SelectNodes(\"ns:p\", nsMgr)!)\n        {\n            var _begin = _p.GetAttribute(\"begin\");\n            var _end = _p.GetAttribute(\"end\");\n            // Handle namespace\n            foreach (XmlAttribute attr in _p.Attributes)\n            {\n                if (attr.LocalName == \"begin\") _begin = attr.Value;\n                else if (attr.LocalName == \"end\") _end = attr.Value;\n            }\n            _p.SetAttribute(\"begin\", Add(_begin));\n            _p.SetAttribute(\"end\", Add(_end));\n            // Console.WriteLine($\"{_begin} {_p.GetAttribute(\"begin\")}\");\n            // Console.WriteLine($\"{_end} {_p.GetAttribute(\"begin\")}\");\n        }\n\n        return xmlDoc.OuterXml;\n    }\n\n    private static string GetTextFromElement(XmlElement node)\n    {\n        var sb = new StringBuilder();\n        foreach (XmlNode item in node.ChildNodes)\n        {\n            if (item.NodeType == XmlNodeType.Text)\n            {\n                sb.Append(item.InnerText.Trim());\n            }\n            else if(item is { NodeType: XmlNodeType.Element, Name: \"br\" })\n            {\n                sb.AppendLine();\n            }\n        }\n        return sb.ToString();\n    }\n\n    private static List<string> SplitMultipleRootElements(string xml)\n    {\n        return !MultiElementsFixRegex().IsMatch(xml) ? [] : MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList();\n    }\n\n    public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L)\n    {\n        return ExtractFromMp4s([item], segTimeMs, baseTimestamp);\n    }\n\n    private static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)\n    {\n        // read ttmls\n        List<string> xmls = [];\n        int segIndex = 0;\n        foreach (var item in items)\n        {\n            var dataSeg = File.ReadAllBytes(item);\n\n            var sawMDAT = false;\n            // parse media\n            new MP4Parser()\n                .Box(\"mdat\", MP4Parser.AllData(data =>\n                {\n                    sawMDAT = true;\n                    // Join this to any previous payload, in case the mp4 has multiple\n                    // mdats.\n                    if (segTimeMs != 0)\n                    {\n                        var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));\n                        foreach (var item in datas)\n                        {\n                            xmls.Add(ShiftTime(item, segTimeMs, segIndex));\n                        }\n                    }\n                    else\n                    {\n                        var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));\n                        xmls.AddRange(datas);\n                    }\n                }))\n                .Parse(dataSeg,/* partialOkay= */ false);\n            segIndex++;\n        }\n\n        return ExtractSub(xmls, baseTimestamp);\n    }\n\n    public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L)\n    {\n        return ExtractFromTTMLs([item], segTimeMs, baseTimestamp);\n    }\n\n    public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)\n    {\n        // read ttmls\n        List<string> xmls = [];\n        int segIndex = 0;\n        foreach (var item in items)\n        {\n            var xml = File.ReadAllText(item);\n            xmls.Add(segTimeMs != 0 ? ShiftTime(xml, segTimeMs, segIndex) : xml);\n            segIndex++;\n        }\n\n        return ExtractSub(xmls, baseTimestamp);\n    }\n\n    private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp)\n    {\n        // parsing\n        var xmlDoc = new XmlDocument();\n        var finalSubs = new List<SubEntity>();\n        XmlNode? headNode = null;\n        XmlNamespaceManager? nsMgr = null;\n        var regex = LabelFixRegex();\n        var attrRegex = AttrRegex();\n        foreach (var item in xmls)\n        {\n            var xmlContent = item;\n            if (!xmlContent.Contains(\"<tt\")) continue;\n\n            // fix non-standard xml \n            var xmlContentFix = xmlContent;\n            if (regex.IsMatch(xmlContent))\n            {\n                foreach (Match m in regex.Matches(xmlContentFix))\n                {\n                    try\n                    {\n                        var inner = m.Groups[1].Value;\n                        if (attrRegex.IsMatch(inner))\n                        {\n                            inner = attrRegex.Replace(inner, \"\");\n                        }\n                        new XmlDocument().LoadXml($\"<p>{inner}</p>\");\n                    }\n                    catch (Exception)\n                    {\n                        xmlContentFix = xmlContentFix.Replace(m.Groups[1].Value, System.Web.HttpUtility.HtmlEncode(m.Groups[1].Value));\n                    }\n                }\n            }\n            xmlDoc.LoadXml(xmlContentFix);\n            var ttNode = xmlDoc.LastChild;\n            if (nsMgr == null)\n            {\n                var ns = ((XmlElement)ttNode!).GetAttribute(\"xmlns\");\n                nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);\n                nsMgr.AddNamespace(\"ns\", ns);\n            }\n            if (headNode == null)\n                headNode = ttNode!.SelectSingleNode(\"ns:head\", nsMgr);\n\n            var bodyNode = ttNode!.SelectSingleNode(\"ns:body\", nsMgr);\n            if (bodyNode == null)\n                continue;\n\n            var _div = bodyNode.SelectSingleNode(\"ns:div\", nsMgr);\n            if (_div == null)\n                continue;\n\n\n            // PNG Subs\n            var imageDic = new Dictionary<string, string>(); // id, Base64\n            if (ImageRegex().IsMatch(xmlDoc.InnerXml))\n            {\n                foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))\n                {\n                    imageDic.Add(img.Groups[1].Value.Trim(), img.Groups[2].Value.Trim());\n                }\n            }\n\n            // convert <div> to <p>\n            if (_div!.SelectNodes(\"ns:p\", nsMgr) == null || _div!.SelectNodes(\"ns:p\", nsMgr)!.Count == 0)\n            {\n                foreach (XmlElement _tDiv in bodyNode.SelectNodes(\"ns:div\", nsMgr)!)\n                {\n                    var _p = xmlDoc.CreateDocumentFragment();\n                    _p.InnerXml = _tDiv.OuterXml.Replace(\"<div \", \"<p \").Replace(\"</div>\", \"</p>\");\n                    _div.AppendChild(_p);\n                }\n            }\n\n            // Parse <p> label\n            foreach (XmlElement _p in _div!.SelectNodes(\"ns:p\", nsMgr)!)\n            {\n                var _begin = _p.GetAttribute(\"begin\");\n                var _end = _p.GetAttribute(\"end\");\n                var _region = _p.GetAttribute(\"region\");\n                var _bgImg = _p.GetAttribute(\"smpte:backgroundImage\");\n                // Handle namespace\n                foreach (XmlAttribute attr in _p.Attributes)\n                {\n                    if (attr.LocalName == \"begin\") _begin = attr.Value;\n                    else if (attr.LocalName == \"end\") _end = attr.Value;\n                    else if (attr.LocalName == \"region\") _region = attr.Value;\n                }\n                var sub = new SubEntity\n                {\n                    Begin = _begin,\n                    End = _end,\n                    Region = _region\n                };\n\n                if (string.IsNullOrEmpty(_bgImg))\n                {\n                    var _spans = _p.ChildNodes;\n                    // Collect <span>\n                    foreach (XmlNode _node in _spans)\n                    {\n                        if (_node.NodeType == XmlNodeType.Element)\n                        {\n                            var _span = (XmlElement)_node;\n                            if (string.IsNullOrEmpty(_span.InnerText))\n                                continue;\n                            sub.Contents.Add(_span);\n                            sub.ContentStrings.Add(_span.OuterXml);\n                        }\n                        else if (_node.NodeType == XmlNodeType.Text)\n                        {\n                            var _span = new XmlDocument().CreateElement(\"span\");\n                            _span.InnerText = _node.Value!;\n                            sub.Contents.Add(_span);\n                            sub.ContentStrings.Add(_span.OuterXml);\n                        }\n                    }\n                }\n                else\n                {\n                    var id = _bgImg.Replace(\"#\", \"\");\n                    if (imageDic.TryGetValue(id, out var value))\n                    {\n                        var _span = new XmlDocument().CreateElement(\"span\");\n                        _span.InnerText = $\"Base64::{value}\";\n                        sub.Contents.Add(_span);\n                        sub.ContentStrings.Add(_span.OuterXml);\n                    }\n                }\n                    \n                // Check if one <p> has been splitted\n                var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));\n                // Skip empty lines\n                if (sub.ContentStrings.Count <= 0)\n                    continue;\n                // Extend <p> duration\n                if (index != -1)\n                    finalSubs[index].End = sub.End;\n                else if (!finalSubs.Contains(sub))\n                    finalSubs.Add(sub);\n            }\n        }\n\n\n        var dic = new Dictionary<string, string>();\n        foreach (var sub in finalSubs)\n        {\n            var key = $\"{sub.Begin} --> {sub.End}\";\n            foreach (var item in sub.Contents)\n            {\n                if (dic.ContainsKey(key))\n                {\n                    if (item.GetAttribute(\"tts:fontStyle\") == \"italic\" || item.GetAttribute(\"tts:fontStyle\") == \"oblique\")\n                        dic[key] = $\"{dic[key]}\\r\\n<i>{GetTextFromElement(item)}</i>\";\n                    else\n                        dic[key] = $\"{dic[key]}\\r\\n{GetTextFromElement(item)}\";\n                }\n                else\n                {\n                    if (item.GetAttribute(\"tts:fontStyle\") == \"italic\" || item.GetAttribute(\"tts:fontStyle\") == \"oblique\")\n                        dic.Add(key, $\"<i>{GetTextFromElement(item)}</i>\");\n                    else\n                        dic.Add(key, GetTextFromElement(item));\n                }\n            }\n        }\n\n\n        var vtt = new StringBuilder();\n        vtt.AppendLine(\"WEBVTT\");\n        foreach (var item in dic)\n        {\n            vtt.AppendLine(item.Key);\n            vtt.AppendLine(item.Value);\n            vtt.AppendLine();\n        }\n\n        return WebVttSub.Parse(vtt.ToString(), baseTimestamp);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MP4VttUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing System.Text;\n\nnamespace Mp4SubtitleParser;\n\npublic static class MP4VttUtil\n{\n    public static (bool, uint) CheckInit(byte[] data)\n    {\n        uint timescale = 0;\n        bool sawWVTT = false;\n\n        // parse init\n        new MP4Parser()\n            .Box(\"moov\", MP4Parser.Children)\n            .Box(\"trak\", MP4Parser.Children)\n            .Box(\"mdia\", MP4Parser.Children)\n            .FullBox(\"mdhd\", box =>\n            {\n                if (box.Version is not (0 or 1))\n                    throw new Exception(\"MDHD version can only be 0 or 1\");\n                timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);\n            })\n            .Box(\"minf\", MP4Parser.Children)\n            .Box(\"stbl\", MP4Parser.Children)\n            .FullBox(\"stsd\", MP4Parser.SampleDescription)\n            .Box(\"wvtt\", _ => {\n                // A valid vtt init segment, though we have no actual subtitles yet.\n                sawWVTT = true;\n            })\n            .Parse(data);\n\n        return (sawWVTT, timescale);\n    }\n\n    public static WebVttSub ExtractSub(IEnumerable<string> files, uint timescale)\n    {\n        if (timescale == 0)\n            throw new Exception(\"Missing timescale for VTT content!\");\n\n        List<SubCue> cues = [];\n\n        foreach (var item in files)\n        {\n            var dataSeg = File.ReadAllBytes(item);\n\n            bool sawTFDT = false;\n            bool sawTRUN = false;\n            bool sawMDAT = false;\n            byte[]? rawPayload = null;\n            ulong baseTime = 0;\n            ulong defaultDuration = 0;\n            List<Sample> presentations = [];\n\n\n            // parse media\n            new MP4Parser()\n                .Box(\"moof\", MP4Parser.Children)\n                .Box(\"traf\", MP4Parser.Children)\n                .FullBox(\"tfdt\", box =>\n                {\n                    sawTFDT = true;\n                    if (box.Version is not (0 or 1))\n                        throw new Exception(\"TFDT version can only be 0 or 1\");\n                    baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);\n                })\n                .FullBox(\"tfhd\", box =>\n                {\n                    if (box.Flags == 1000)\n                        throw new Exception(\"A TFHD box should have a valid flags value\");\n                    defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;\n                })\n                .FullBox(\"trun\", box =>\n                {\n                    sawTRUN = true;\n                    if (box.Version == 1000)\n                        throw new Exception(\"A TRUN box should have a valid version value\");\n                    if (box.Flags == 1000)\n                        throw new Exception(\"A TRUN box should have a valid flags value\");\n                    presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;\n                })\n                .Box(\"mdat\", MP4Parser.AllData(data =>\n                {\n                    if (sawMDAT)\n                        throw new Exception(\"VTT cues in mp4 with multiple MDAT are not currently supported\");\n                    sawMDAT = true;\n                    rawPayload = data;\n                }))\n                .Parse(dataSeg,/* partialOkay= */ false);\n\n            if (!sawMDAT && !sawTFDT && !sawTRUN)\n            {\n                throw new Exception(\"A required box is missing\");\n            }\n\n            var currentTime = baseTime;\n            var reader = new BinaryReader2(new MemoryStream(rawPayload!));\n\n            foreach (var presentation in presentations)\n            {\n                var duration = presentation.SampleDuration == 0 ? defaultDuration : presentation.SampleDuration;\n                var startTime = presentation.SampleCompositionTimeOffset != 0 ?\n                    baseTime + presentation.SampleCompositionTimeOffset :\n                    currentTime;\n                currentTime = startTime + duration;\n                var totalSize = 0;\n                do\n                {\n                    // Read the payload size.\n                    var payloadSize = (int)reader.ReadUInt32();\n                    totalSize += payloadSize;\n\n                    // Skip the type.\n                    var payloadType = reader.ReadUInt32();\n                    var payloadName = MP4Parser.TypeToString(payloadType);\n\n                    // Read the data payload.\n                    byte[]? payload = null;\n                    if (payloadName == \"vttc\")\n                    {\n                        if (payloadSize > 8)\n                        {\n                            payload = reader.ReadBytes(payloadSize - 8);\n                        }\n                    }\n                    else if (payloadName == \"vtte\")\n                    {\n                        // It's a vtte, which is a vtt cue that is empty. Ignore any data that\n                        // does exist.\n                        reader.ReadBytes(payloadSize - 8);\n                    }\n                    else\n                    {\n                        Console.WriteLine($\"Unknown box {payloadName}! Skipping!\");\n                        reader.ReadBytes(payloadSize - 8);\n                    }\n\n                    if (duration != 0)\n                    {\n                        if (payload != null)\n                        {\n                            var cue = ParseVTTC(\n                                payload,\n                                0 + (double)startTime / timescale,\n                                0 + (double)currentTime / timescale);\n                            // Check if same subtitle has been splitted\n                            if (cue != null)\n                            {\n                                var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload);\n                                if (index != -1)\n                                {\n                                    cues[index].EndTime = cue.EndTime;\n                                }\n                                else\n                                {\n                                    cues.Add(cue);\n                                }\n                            }\n                        }\n                    }\n                    else\n                    {\n                        throw new Exception(\"WVTT sample duration unknown, and no default found!\");\n                    }\n\n                    if (!(presentation.SampleSize == 0 || totalSize <= presentation.SampleSize))\n                    {\n                        throw new Exception(\"The samples do not fit evenly into the sample sizes given in the TRUN box!\");\n                    }\n\n                } while (presentation.SampleSize != 0 && (totalSize < presentation.SampleSize));\n\n                if (reader.HasMoreData())\n                {\n                    // throw new Exception(\"MDAT which contain VTT cues and non-VTT data are not currently supported!\");\n                }\n            }\n        }\n\n        if (cues.Count > 0)\n        {\n            return new WebVttSub() { Cues = cues };\n        }\n        return new WebVttSub();\n    }\n\n    private static SubCue? ParseVTTC(byte[] data, double startTime, double endTime)\n    {\n        string payload = string.Empty;\n        string id = string.Empty;\n        string settings = string.Empty;\n        new MP4Parser()\n            .Box(\"payl\", MP4Parser.AllData(data =>\n            {\n                payload = Encoding.UTF8.GetString(data);\n            }))\n            .Box(\"iden\", MP4Parser.AllData(data =>\n            {\n                id = Encoding.UTF8.GetString(data);\n            }))\n            .Box(\"sttg\", MP4Parser.AllData(data =>\n            {\n                settings = Encoding.UTF8.GetString(data);\n            }))\n            .Parse(data);\n\n        if (!string.IsNullOrEmpty(payload))\n        {\n            return new SubCue() { StartTime = TimeSpan.FromSeconds(startTime), EndTime = TimeSpan.FromSeconds(endTime), Payload = payload, Settings = settings };\n        }\n        return null;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Mp4/MSSMoovProcessor.cs",
    "content": "﻿using Mp4SubtitleParser;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Util;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\n// https://github.com/canalplus/rx-player/blob/48d1f845064cea5c5a3546d2c53b1855c2be149d/src/parsers/manifest/smooth/get_codecs.ts\n// https://github.dev/Dash-Industry-Forum/dash.js/blob/2aad3e79079b4de0bcd961ce6b4957103d98a621/src/mss/MssFragmentMoovProcessor.js\n// https://github.com/yt-dlp/yt-dlp/blob/3639df54c3298e35b5ae2a96a25bc4d3c38950d0/yt_dlp/downloader/ism.py\n// https://github.com/google/ExoPlayer/blob/a9444c880230d2c2c79097e89259ce0b9f80b87d/library/extractor/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java#L38\n// https://github.com/sannies/mp4parser/blob/master/isoparser/src/main/java/org/mp4parser/boxes/iso14496/part15/HevcDecoderConfigurationRecord.java\nnamespace N_m3u8DL_RE.Parser.Mp4;\n\npublic partial class MSSMoovProcessor\n{\n    [GeneratedRegex(@\"\\<KID\\>(.*?)\\<\")]\n    private static partial Regex KIDRegex();\n\n    private static string StartCode = \"00000001\";\n    private StreamSpec StreamSpec;\n    private int TrackId = 2;\n    private string FourCC;\n    private string CodecPrivateData;\n    private int Timesacle;\n    private long Duration;\n    private string Language => StreamSpec.Language ?? \"und\";\n    private int Width => int.Parse((StreamSpec.Resolution ?? \"0x0\").Split('x').First());\n    private int Height => int.Parse((StreamSpec.Resolution ?? \"0x0\").Split('x').Last());\n    private string StreamType;\n    private int Channels;\n    private int BitsPerSample;\n    private int SamplingRate;\n    private int NalUnitLengthField;\n    private long CreationTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();\n\n    private bool IsProtection;\n    private string ProtectionSystemId;\n    private string ProtectionData;\n    private string? ProtecitonKID;\n    private string? ProtecitonKID_PR;\n    private byte[] UnityMatrix\n    {\n        get\n        {\n            using var stream = new MemoryStream();\n            using var writer = new BinaryWriter2(stream);\n            writer.WriteInt(0x10000);\n            writer.WriteInt(0);\n            writer.WriteInt(0);\n            writer.WriteInt(0);\n            writer.WriteInt(0x10000);\n            writer.WriteInt(0);\n            writer.WriteInt(0);\n            writer.WriteInt(0);\n            writer.WriteInt(0x40000000);\n            return stream.ToArray();\n        }\n    }\n    private static byte TRACK_ENABLED = 0x1;\n    private static byte TRACK_IN_MOVIE = 0x2;\n    private static byte TRACK_IN_PREVIEW = 0x4;\n    private static byte SELF_CONTAINED = 0x1;\n\n    private static List<string> SupportedFourCC =\n        [\"HVC1\", \"HEV1\", \"AACL\", \"AACH\", \"EC-3\", \"H264\", \"AVC1\", \"DAVC\", \"AVC1\", \"TTML\", \"DVHE\", \"DVH1\"];\n\n    public MSSMoovProcessor(StreamSpec streamSpec)\n    {\n        this.StreamSpec = streamSpec;\n        var data = streamSpec.MSSData!;\n        this.NalUnitLengthField = data.NalUnitLengthField;\n        this.CodecPrivateData = data.CodecPrivateData;\n        this.FourCC = data.FourCC;\n        this.Timesacle = data.Timesacle;\n        this.Duration = data.Duration;\n        this.StreamType = data.Type;\n        this.Channels = data.Channels;\n        this.SamplingRate = data.SamplingRate;\n        this.BitsPerSample = data.BitsPerSample;\n        this.IsProtection = data.IsProtection;\n        this.ProtectionData = data.ProtectionData;\n        this.ProtectionSystemId = data.ProtectionSystemID;\n\n        // 需要手动生成CodecPrivateData\n        if (string.IsNullOrEmpty(CodecPrivateData))\n        {\n            GenCodecPrivateDataForAAC();\n        }\n\n        // 解析KID\n        if (IsProtection)\n        {\n            ExtractKID();\n        }\n    }\n\n    private static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = [\"\", \"A\", \"B\", \"C\"];\n    private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch\n    {\n        96000 => 0x0,\n        88200 => 0x1,\n        64000 => 0x2,\n        48000 => 0x3,\n        44100 => 0x4,\n        32000 => 0x5,\n        24000 => 0x6,\n        22050 => 0x7,\n        16000 => 0x8,\n        12000 => 0x9,\n        11025 => 0xA,\n        8000 => 0xB,\n        7350 => 0xC,\n        _ => 0x0\n    };\n\n    private void GenCodecPrivateDataForAAC()\n    {\n        var objectType = 0x02; // AAC Main Low Complexity => object Type = 2\n        var indexFreq = SamplingFrequencyIndex(SamplingRate);\n\n        if (FourCC == \"AACH\")\n        {\n            // 4 bytes :     XXXXX         XXXX          XXXX             XXXX                  XXXXX      XXX   XXXXXXX\n            //           ' ObjectType' 'Freq Index' 'Channels value'   'Extens Sampl Freq'  'ObjectType'  'GAS' 'alignment = 0'\n            objectType = 0x05; // High Efficiency AAC Profile = object Type = 5 SBR\n            var codecPrivateData = new byte[4];\n            var extensionSamplingFrequencyIndex = SamplingFrequencyIndex(SamplingRate * 2); // in HE AAC Extension Sampling frequence\n            // equals to SamplingRate*2\n            // Freq Index is present for 3 bits in the first byte, last bit is in the second\n            codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1));\n            codecPrivateData[1] = (byte)((indexFreq << 7) | (Channels << 3) | (extensionSamplingFrequencyIndex >> 1));\n            codecPrivateData[2] = (byte)((extensionSamplingFrequencyIndex << 7) | (0x02 << 2)); // origin object type equals to 2 => AAC Main Low Complexity\n            codecPrivateData[3] = 0x0; // alignment bits\n\n            var arr16 = new ushort[2];\n            arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]);\n            arr16[1] = (ushort)((codecPrivateData[2] << 8) + codecPrivateData[3]);\n\n            // convert decimal to hex value\n            this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0');\n            this.CodecPrivateData += HexUtil.BytesToHex(BitConverter.GetBytes(arr16[1])).PadLeft(16, '0');\n        }\n        else if (FourCC.StartsWith(\"AAC\")) \n        {\n            // 2 bytes :     XXXXX         XXXX          XXXX              XXX\n            //           ' ObjectType' 'Freq Index' 'Channels value'   'GAS = 000'\n            var codecPrivateData = new byte[2];\n            // Freq Index is present for 3 bits in the first byte, last bit is in the second\n            codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1));\n            codecPrivateData[1] = (byte)((indexFreq << 7) | Channels << 3);\n            // put the 2 bytes in an 16 bits array\n            var arr16 = new ushort[1];\n            arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]);\n\n            // convert decimal to hex value\n            this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0');\n        }\n    }\n\n    private void ExtractKID()\n    {\n        // playready\n        if (ProtectionSystemId.ToUpper() == \"9A04F079-9840-4286-AB92-E65BE0885F95\")\n        {\n            var bytes = HexUtil.HexToBytes(ProtectionData.Replace(\"00\", \"\"));\n            var text = Encoding.ASCII.GetString(bytes);\n            var kidBytes = Convert.FromBase64String(KIDRegex().Match(text).Groups[1].Value);\n            // save kid for playready\n            this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes);\n            // fix byte order\n            var reverse1 = new[] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] };\n            var reverse2 = new[] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] };\n            Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length);\n            Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length);\n            this.ProtecitonKID = HexUtil.BytesToHex(kidBytes);\n        }\n        // widevine\n        else if (ProtectionSystemId.ToUpper() == \"EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED\")\n        {\n            throw new NotSupportedException();\n        }\n    }\n\n    public static bool CanHandle(string fourCC) => SupportedFourCC.Contains(fourCC);\n\n    private byte[] Box(string boxType, byte[] payload)\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteUInt(8 + (uint)payload.Length);\n        writer.Write(boxType);\n        writer.Write(payload);\n\n        return stream.ToArray();\n    }\n\n    private byte[] FullBox(string boxType, byte version, uint flags, byte[] payload)\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.Write(version);\n        writer.WriteUInt(flags, offset: 1);\n        writer.Write(payload);\n\n        return Box(boxType, stream.ToArray());\n    }\n\n    private byte[] GenSinf(string codec)\n    {\n        var frmaBox = Box(\"frma\", Encoding.ASCII.GetBytes(codec));\n\n        var sinfPayload = new List<byte>();\n        sinfPayload.AddRange(frmaBox);\n\n        var schmPayload = new List<byte>();\n        schmPayload.AddRange(Encoding.ASCII.GetBytes(\"cenc\")); // scheme_type 'cenc' => common encryption\n        schmPayload.AddRange([0, 1, 0, 0]); // scheme_version Major version 1, Minor version 0\n        var schmBox = FullBox(\"schm\", 0, 0, schmPayload.ToArray());\n\n        sinfPayload.AddRange(schmBox);\n\n        var tencPayload = new List<byte>();\n        tencPayload.AddRange([0, 0]);\n        tencPayload.Add(0x1); // default_IsProtected\n        tencPayload.Add(0x8); // default_Per_Sample_IV_size\n        tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); // default_KID\n        // tencPayload.Add(0x8);// default_constant_IV_size\n        // tencPayload.AddRange(new byte[8]);// default_constant_IV\n        var tencBox = FullBox(\"tenc\", 0, 0, tencPayload.ToArray());\n\n        var schiBox = Box(\"schi\", tencBox);\n        sinfPayload.AddRange(schiBox);\n\n        var sinfBox = Box(\"sinf\", sinfPayload.ToArray());\n\n        return sinfBox;\n    }\n\n    private byte[] GenFtyp()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.Write(\"isml\"); // major brand\n        writer.WriteUInt(1); // minor version\n        writer.Write(\"iso5\"); // compatible brand\n        writer.Write(\"iso6\"); // compatible brand\n        writer.Write(\"piff\"); // compatible brand\n        writer.Write(\"msdh\"); // compatible brand\n\n        return Box(\"ftyp\", stream.ToArray());\n    }\n\n    private byte[] GenMvhd()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteULong(CreationTime); // creation_time\n        writer.WriteULong(CreationTime); // modification_time\n        writer.WriteUInt(Timesacle); // timescale\n        writer.WriteULong(Duration); // duration\n        writer.WriteUShort(1, padding: 2); // rate\n        writer.WriteByte(1, padding: 1); // volume\n        writer.WriteUShort(0); // reserved\n        writer.WriteUInt(0);\n        writer.WriteUInt(0);\n\n        writer.Write(UnityMatrix);\n\n        writer.WriteUInt(0); // pre defined\n        writer.WriteUInt(0);\n        writer.WriteUInt(0);\n        writer.WriteUInt(0);\n        writer.WriteUInt(0);\n        writer.WriteUInt(0);\n\n        writer.WriteUInt(0xffffffff); // next track id\n\n\n        return FullBox(\"mvhd\", 1, 0, stream.ToArray());\n    }\n\n    private byte[] GenTkhd()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteULong(CreationTime); // creation_time\n        writer.WriteULong(CreationTime); // modification_time\n        writer.WriteUInt(TrackId); // track id\n        writer.WriteUInt(0); // reserved\n        writer.WriteULong(Duration); // duration\n        writer.WriteUInt(0); // reserved\n        writer.WriteUInt(0);\n        writer.WriteShort(0); // layer\n        writer.WriteShort(0); // alternate group\n        writer.WriteByte(StreamType == \"audio\" ? (byte)1 : (byte)0, padding: 1); // volume\n        writer.WriteUShort(0); // reserved\n\n        writer.Write(UnityMatrix);\n\n        writer.WriteUShort(Width, padding: 2); // width\n        writer.WriteUShort(Height, padding: 2); // height\n\n        return FullBox(\"tkhd\", 1, (uint)TRACK_ENABLED | TRACK_IN_MOVIE | TRACK_IN_PREVIEW, stream.ToArray());\n    }\n\n\n    private byte[] GenMdhd()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteULong(CreationTime); // creation_time\n        writer.WriteULong(CreationTime); // modification_time\n        writer.WriteUInt(Timesacle); // timescale\n        writer.WriteULong(Duration); // duration\n        writer.WriteUShort((Language[0] - 0x60) << 10 | (Language[1] - 0x60) << 5 | (Language[2] - 0x60)); // language\n        writer.WriteUShort(0); // pre defined\n\n        return FullBox(\"mdhd\", 1, 0, stream.ToArray());\n    }\n\n    private byte[] GenHdlr()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteUInt(0); // pre defined\n        if (StreamType == \"audio\") writer.Write(\"soun\");\n        else if (StreamType == \"video\") writer.Write(\"vide\");\n        else if (StreamType == \"text\") writer.Write(\"subt\");\n        else throw new NotSupportedException();\n\n        writer.WriteUInt(0); // reserved\n        writer.WriteUInt(0);\n        writer.WriteUInt(0);\n        writer.Write($\"{StreamSpec.GroupId ?? \"RE Handler\"}\\0\"); // name\n\n        return FullBox(\"hdlr\", 0, 0, stream.ToArray());\n    }\n\n    private byte[] GenMinf()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        var minfPayload = new List<byte>();\n        if (StreamType == \"audio\")\n        {\n            var smhd = new List<byte>();\n            smhd.Add(0); smhd.Add(0); // balance\n            smhd.Add(0); smhd.Add(0); // reserved\n\n            minfPayload.AddRange(FullBox(\"smhd\", 0, 0, smhd.ToArray())); // Sound Media Header\n        }\n        else if (StreamType == \"video\")\n        {\n            var vmhd = new List<byte>();\n            vmhd.Add(0); vmhd.Add(0); // graphics mode\n            vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0);// opcolor\n\n            minfPayload.AddRange(FullBox(\"vmhd\", 0, 1, vmhd.ToArray())); // Video Media Header\n        }\n        else if (StreamType == \"text\")\n        {\n            minfPayload.AddRange(FullBox(\"sthd\", 0, 0, [])); // Subtitle Media Header\n        }\n        else\n        {\n            throw new NotSupportedException();\n        }\n\n        var drefPayload = new List<byte>();\n        drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); // entry count\n        drefPayload.AddRange(FullBox(\"url \", 0, SELF_CONTAINED, [])); // Data Entry URL Box\n\n        var dinfPayload = FullBox(\"dref\", 0, 0, drefPayload.ToArray()); // Data Reference Box\n        minfPayload.AddRange(Box(\"dinf\", dinfPayload.ToArray())); // Data Information Box\n\n        return minfPayload.ToArray();\n    }\n\n    private byte[] GenEsds(byte[] audioSpecificConfig)\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        // ESDS length = esds box header length (= 12) +\n        //               ES_Descriptor header length (= 5) +\n        //               DecoderConfigDescriptor header length (= 15) +\n        //               decoderSpecificInfo header length (= 2) +\n        //               AudioSpecificConfig length (= codecPrivateData length)\n        // esdsLength = 34 + len(audioSpecificConfig)\n\n        // ES_Descriptor (see ISO/IEC 14496-1 (Systems))\n        writer.WriteByte(0x03); // tag = 0x03 (ES_DescrTag)\n        writer.WriteByte((byte)(20 + audioSpecificConfig.Length)); // size\n        writer.WriteByte((byte)((TrackId & 0xFF00) >> 8)); // ES_ID = track_id\n        writer.WriteByte((byte)(TrackId & 0x00FF));\n        writer.WriteByte(0); // flags and streamPriority\n\n        // DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems))\n        writer.WriteByte(0x04); // tag = 0x04 (DecoderConfigDescrTag)\n        writer.WriteByte((byte)(15 + audioSpecificConfig.Length)); // size\n        writer.WriteByte(0x40); // objectTypeIndication = 0x40 (MPEG-4 AAC)\n        writer.WriteByte((0x05 << 2) | (0 << 1) | 1); // reserved = 1\n        writer.WriteByte(0xFF); // buffersizeDB = undefined\n        writer.WriteByte(0xFF); \n        writer.WriteByte(0xFF);\n\n        var bandwidth = StreamSpec.Bandwidth!;\n        writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); // maxBitrate\n        writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16));\n        writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8));\n        writer.WriteByte((byte)(bandwidth  & 0x000000FF));\n        writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); // avgbitrate\n        writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16)); \n        writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8));\n        writer.WriteByte((byte)(bandwidth  & 0x000000FF));\n\n        // DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems))\n        writer.WriteByte(0x05); // tag = 0x05 (DecSpecificInfoTag)\n        writer.WriteByte((byte)audioSpecificConfig.Length); // size\n        writer.Write(audioSpecificConfig); // AudioSpecificConfig bytes\n\n        return FullBox(\"esds\", 0, 0, stream.ToArray());\n    }\n\n    private byte[] GetSampleEntryBox()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteByte(0); // reserved\n        writer.WriteByte(0);\n        writer.WriteByte(0);\n        writer.WriteByte(0);\n        writer.WriteByte(0);\n        writer.WriteByte(0);\n        writer.WriteUShort(1); // data reference index\n\n        if (StreamType == \"audio\")\n        {\n            writer.WriteUInt(0); // reserved2\n            writer.WriteUInt(0);\n            writer.WriteUShort(Channels); // channels\n            writer.WriteUShort(BitsPerSample); // bits_per_sample\n            writer.WriteUShort(0); // pre defined\n            writer.WriteUShort(0); // reserved3\n            writer.WriteUShort(SamplingRate, padding: 2); // sampling_rate\n\n            var audioSpecificConfig = HexUtil.HexToBytes(CodecPrivateData);\n            var esdsBox = GenEsds(audioSpecificConfig);\n            writer.Write(esdsBox);\n\n            if (FourCC.StartsWith(\"AAC\")) \n            {\n                if (IsProtection)\n                {\n                    var sinfBox = GenSinf(\"mp4a\");\n                    writer.Write(sinfBox);\n                    return Box(\"enca\", stream.ToArray()); // Encrypted Audio\n                }\n                return Box(\"mp4a\", stream.ToArray());\n            }\n            if (FourCC == \"EC-3\")\n            {\n                if (IsProtection)\n                {\n                    var sinfBox = GenSinf(\"ec-3\");\n                    writer.Write(sinfBox);\n                    return Box(\"enca\", stream.ToArray()); // Encrypted Audio\n                }\n                return Box(\"ec-3\", stream.ToArray());\n            }\n        }\n        else if (StreamType == \"video\")\n        {\n            writer.WriteUShort(0); // pre defined\n            writer.WriteUShort(0); // reserved\n            writer.WriteUInt(0); // pre defined\n            writer.WriteUInt(0);\n            writer.WriteUInt(0);\n            writer.WriteUShort(Width); // width\n            writer.WriteUShort(Height); // height\n            writer.WriteUShort(0x48, padding: 2); // horiz resolution 72 dpi\n            writer.WriteUShort(0x48, padding: 2); // vert resolution 72 dpi\n            writer.WriteUInt(0); // reserved\n            writer.WriteUShort(1); // frame count\n            for (int i = 0; i < 32; i++) // compressor name\n            {\n                writer.WriteByte(0);\n            }\n            writer.WriteUShort(0x18); // depth\n            writer.WriteUShort(65535); // pre defined\n\n            var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData);\n\n            if (FourCC is \"H264\" or \"AVC1\" or \"DAVC\")\n            {\n                var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);\n                var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7));\n                var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8));\n                // make avcC\n                var avcC = GetAvcC(sps, pps);\n                writer.Write(avcC);\n                if (IsProtection)\n                {\n                    var sinfBox = GenSinf(\"avc1\");\n                    writer.Write(sinfBox);\n                    return Box(\"encv\", stream.ToArray()); // Encrypted Video\n                }\n                return Box(\"avc1\", stream.ToArray()); // AVC Simple Entry\n            }\n            if (FourCC is \"HVC1\" or \"HEV1\")\n            {\n                var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);\n                var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));\n                var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));\n                var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));\n                // make hvcC\n                var hvcC = GetHvcC(sps, pps, vps);\n                writer.Write(hvcC);\n                if (IsProtection)\n                {\n                    var sinfBox = GenSinf(\"hvc1\");\n                    writer.Write(sinfBox);\n                    return Box(\"encv\", stream.ToArray()); // Encrypted Video\n                }\n                return Box(\"hvc1\", stream.ToArray()); // HEVC Simple Entry\n            }\n            // 杜比视界也按照hevc处理\n            if (FourCC is \"DVHE\" or \"DVH1\")\n            {\n                var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);\n                var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));\n                var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));\n                var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));\n                // make hvcC\n                var hvcC = GetHvcC(sps, pps, vps, \"dvh1\");\n                writer.Write(hvcC);\n                if (IsProtection)\n                {\n                    var sinfBox = GenSinf(\"dvh1\");\n                    writer.Write(sinfBox);\n                    return Box(\"encv\", stream.ToArray()); // Encrypted Video\n                }\n                return Box(\"dvh1\", stream.ToArray()); // HEVC Simple Entry\n            }\n\n            throw new NotSupportedException();\n        }\n        else if (StreamType == \"text\")\n        {\n            if (FourCC == \"TTML\")\n            {\n                writer.Write(\"http://www.w3.org/ns/ttml\\0\"); // namespace\n                writer.Write(\"\\0\"); // schema location\n                writer.Write(\"\\0\"); // auxilary mime types(??)\n                return Box(\"stpp\", stream.ToArray()); // TTML Simple Entry\n            }\n            throw new NotSupportedException();\n        }\n        else\n        {\n            throw new NotSupportedException();\n        }\n\n        throw new NotSupportedException();\n    }\n\n    private byte[] GetAvcC(byte[] sps, byte[] pps)\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteByte(1); // configuration version\n        writer.Write(sps[1..4]); // avc profile indication + profile compatibility + avc level indication\n        writer.WriteByte((byte)(0xfc | (NalUnitLengthField - 1))); // complete representation (1) + reserved (11111) + length size minus one\n        writer.WriteByte(1); // reserved (0) + number of sps (0000001)\n        writer.WriteUShort(sps.Length);\n        writer.Write(sps);\n        writer.WriteByte(1); // number of pps\n        writer.WriteUShort(pps.Length);\n        writer.Write(pps);\n\n        return Box(\"avcC\", stream.ToArray()); // AVC Decoder Configuration Record\n    }\n\n    private byte[] GetHvcC(byte[] sps, byte[] pps, byte[] vps, string code = \"hvc1\")\n    {\n        var oriSps = new List<byte>(sps);\n        // https://www.itu.int/rec/dologin.asp?lang=f&id=T-REC-H.265-201504-S!!PDF-E&type=items\n        // Read generalProfileSpace, generalTierFlag, generalProfileIdc,\n        // generalProfileCompatibilityFlags, constraintBytes, generalLevelIdc\n        // from sps\n        var encList = new List<byte>();\n        /**\n         * 处理payload, 有00 00 03 0,1,2,3的情况 统一换成00 00 XX 即丢弃03\n         * 注意：此处采用的逻辑是直接简单粗暴地判断列表末尾3字节，如果是0x000003就删掉最后的0x03，可能会导致以下情况\n         * 00 00 03 03 03 03 03 01 会被直接处理成 => 00 00 01\n         * 此处经过测试只有直接跳过才正常，如果处理成 00 00 03 03 03 03 01 是有问题的\n         *\n         * 测试的数据如下：\n         *   原始：42 01 01 01 60 00 00 03 00 90 00 00 03 00 00 03 00 96 a0 01 e0 20 06 61 65 95 9a 49 30 bf fc 0c 7c 0c 81 a8 08 08 08 20 00 00 03 00 20 00 00 03 03 01\n         * 处理后：42 01 01 01 60 00 00 00 90 00 00 00 00 00 96 A0 01 E0 20 06 61 65 95 9A 49 30 BF FC 0C 7C 0C 81 A8 08 08 08 20 00 00 00 20 00 00 01\n         */\n        using (var _reader = new BinaryReader(new MemoryStream(sps)))\n        {\n            while (_reader.BaseStream.Position < _reader.BaseStream.Length)\n            {\n                encList.Add(_reader.ReadByte());\n                if (encList is [.., 0x00, 0x00, 0x03])\n                {\n                    encList.RemoveAt(encList.Count - 1);\n                }\n            }\n        }\n        sps = encList.ToArray();\n\n        using var reader = new BinaryReader2(new MemoryStream(sps));\n        reader.ReadBytes(2); // Skip 2 bytes unit header\n        var firstByte = reader.ReadByte();\n        var maxSubLayersMinus1 = (firstByte & 0xe) >> 1;\n        var nextByte = reader.ReadByte();\n        var generalProfileSpace = (nextByte & 0xc0) >> 6;\n        var generalTierFlag = (nextByte & 0x20) >> 5;\n        var generalProfileIdc = nextByte & 0x1f;\n        var generalProfileCompatibilityFlags = reader.ReadUInt32();\n        var constraintBytes = reader.ReadBytes(6);\n        var generalLevelIdc = reader.ReadByte();\n\n        /*var skipBit = 0;\n        for (int i = 0; i < maxSubLayersMinus1; i++)\n        {\n            skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag\n        }\n        if (maxSubLayersMinus1 > 0)\n        {\n            for (int i = maxSubLayersMinus1; i < 8; i++)\n            {\n                skipBit += 2; // reserved_zero_2bits\n            }\n        }\n        for (int i = 0; i < maxSubLayersMinus1; i++)\n        {\n            skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag\n        }*/\n\n        // 生成编码信息\n        var codecs = code +\n                     $\".{HEVC_GENERAL_PROFILE_SPACE_STRINGS[generalProfileSpace]}{generalProfileIdc}\" +\n                     $\".{Convert.ToString(generalProfileCompatibilityFlags, 16)}\" +\n                     $\".{(generalTierFlag == 1 ? 'H' : 'L')}{generalLevelIdc}\" +\n                     $\".{HexUtil.BytesToHex(constraintBytes.Where(b => b != 0).ToArray())}\";\n        StreamSpec.Codecs = codecs;\n\n\n        ///////////////////////\n\n\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        // var reserved1 = 0xF;\n\n        writer.WriteByte(1); // configuration version\n        writer.WriteByte((byte)((generalProfileSpace << 6) + (generalTierFlag == 1 ? 0x20 : 0) | generalProfileIdc)); // general_profile_space + general_tier_flag + general_profile_idc\n        writer.WriteUInt(generalProfileCompatibilityFlags); // general_profile_compatibility_flags\n        writer.Write(constraintBytes); // general_constraint_indicator_flags\n        writer.WriteByte((byte)generalProfileIdc); // general_level_idc\n        writer.WriteUShort(0xf000); // reserved + min_spatial_segmentation_idc\n        writer.WriteByte(0xfc); // reserved + parallelismType\n        writer.WriteByte(0 | 0xfc); // reserved + chromaFormat \n        writer.WriteByte(0 | 0xf8); // reserved + bitDepthLumaMinus8\n        writer.WriteByte(0 | 0xf8); // reserved + bitDepthChromaMinus8\n        writer.WriteUShort(0); // avgFrameRate\n        writer.WriteByte((byte)(0 << 6 | 0 << 3 | 0 << 2 | (NalUnitLengthField - 1))); // constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne\n        writer.WriteByte(0x03); // numOfArrays (vps sps pps)\n            \n        sps = oriSps.ToArray();\n        writer.WriteByte(0x20); // array_completeness + reserved + NAL_unit_type\n        writer.WriteUShort(1); // numNalus \n        writer.WriteUShort(vps.Length);\n        writer.Write(vps);\n        writer.WriteByte(0x21);\n        writer.WriteUShort(1); // numNalus\n        writer.WriteUShort(sps.Length);\n        writer.Write(sps);\n        writer.WriteByte(0x22); \n        writer.WriteUShort(1); // numNalus\n        writer.WriteUShort(pps.Length);\n        writer.Write(pps);\n\n        return Box(\"hvcC\", stream.ToArray()); // HEVC Decoder Configuration Record\n    }\n\n    private byte[] GetStsd()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteUInt(1); // entry count\n        var sampleEntryData = GetSampleEntryBox();\n        writer.Write(sampleEntryData);\n\n        return stream.ToArray();\n    }\n\n    private byte[] GetMehd()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteULong(Duration);\n\n        return FullBox(\"mehd\", 1, 0, stream.ToArray()); // Movie Extends Header Box\n    }\n    private byte[] GetTrex()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        writer.WriteUInt(TrackId); // track id\n        writer.WriteUInt(1); // default sample description index\n        writer.WriteUInt(0); // default sample duration\n        writer.WriteUInt(0); // default sample size\n        writer.WriteUInt(0); // default sample flags\n\n        return FullBox(\"trex\", 0, 0, stream.ToArray()); // Track Extends Box\n    }\n\n    private byte[] GenPsshBoxForPlayReady()\n    {\n        using var _stream = new MemoryStream();\n        using var _writer = new BinaryWriter2(_stream);\n        var sysIdData = HexUtil.HexToBytes(ProtectionSystemId.Replace(\"-\", \"\"));\n        var psshData = HexUtil.HexToBytes(ProtectionData);\n\n        _writer.Write(sysIdData);  // SystemID 16 bytes\n        _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes\n        _writer.Write(psshData); // Data\n        var psshBox = FullBox(\"pssh\", 0, 0, _stream.ToArray());\n        return psshBox;\n    }\n\n    private byte[] GenPsshBoxForWideVine()\n    {\n        using var _stream = new MemoryStream();\n        using var _writer = new BinaryWriter2(_stream);\n        var sysIdData = HexUtil.HexToBytes(\"edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\".Replace(\"-\", \"\"));\n        // var kid = HexUtil.HexToBytes(ProtecitonKID);\n\n        _writer.Write(sysIdData);  // SystemID 16 bytes\n        var psshData = HexUtil.HexToBytes($\"08011210{ProtecitonKID}1A046E647265220400000000\");\n        _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes\n        _writer.Write(psshData); // Data\n        var psshBox = FullBox(\"pssh\", 0, 0, _stream.ToArray());\n        return psshBox;\n    }\n\n    private byte[] GenMoof()\n    {\n        using var stream = new MemoryStream();\n        using var writer = new BinaryWriter2(stream);\n\n        // make senc\n        writer.WriteUInt(1); // sample_count\n        writer.Write(new byte[8]); // 8 bytes IV\n\n        var sencBox = FullBox(\"senc\", 1, 0, stream.ToArray());\n\n        var moofBox = Box(\"moof\", sencBox); // Movie Extends Box\n\n        return moofBox;\n    }\n\n    public byte[] GenHeader(byte[] firstSegment)\n    {\n        new MP4Parser()\n            .Box(\"moof\", MP4Parser.Children)\n            .Box(\"traf\", MP4Parser.Children)\n            .FullBox(\"tfhd\", box =>\n            {\n                TrackId = (int)box.Reader.ReadUInt32();\n            })\n            .Parse(firstSegment);\n\n        return GenHeader();\n    }\n\n    public byte[] GenHeader()\n    {\n        using var stream = new MemoryStream();\n\n        var ftyp = GenFtyp(); // File Type Box\n        stream.Write(ftyp);\n\n        var moovPayload = GenMvhd(); // Movie Header Box\n\n        var trakPayload = GenTkhd(); // Track Header Box\n\n        var mdhdPayload = GenMdhd(); // Media Header Box\n\n        var hdlrPayload = GenHdlr(); // Handler Reference Box\n\n        var mdiaPayload = mdhdPayload.Concat(hdlrPayload).ToArray();\n\n        var minfPayload = GenMinf();\n\n\n        var sttsPayload = new byte[] { 0, 0, 0, 0 }; // entry count\n        var stblPayload = FullBox(\"stts\", 0, 0, sttsPayload); // Decoding Time to Sample Box\n\n        var stscPayload = new byte[] { 0, 0, 0, 0 }; // entry count\n        var stscBox = FullBox(\"stsc\", 0, 0, stscPayload); // Sample To Chunk Box\n\n        var stcoPayload = new byte[] { 0, 0, 0, 0 }; // entry count\n        var stcoBox = FullBox(\"stco\", 0, 0, stcoPayload); // Chunk Offset Box\n\n        var stszPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; // sample size, sample count\n        var stszBox = FullBox(\"stsz\", 0, 0, stszPayload); // Sample Size Box\n\n        var stsdPayload = GetStsd();\n        var stsdBox = FullBox(\"stsd\", 0, 0, stsdPayload); // Sample Description Box\n\n        stblPayload = stblPayload.Concat(stscBox).Concat(stcoBox).Concat(stszBox).Concat(stsdBox).ToArray();\n\n\n        var stblBox = Box(\"stbl\", stblPayload); // Sample Table Box\n        minfPayload = minfPayload.Concat(stblBox).ToArray();\n\n        var minfBox = Box(\"minf\", minfPayload); // Media Information Box\n        mdiaPayload = mdiaPayload.Concat(minfBox).ToArray();\n\n        var mdiaBox = Box(\"mdia\", mdiaPayload); // Media Box\n        trakPayload = trakPayload.Concat(mdiaBox).ToArray();\n\n        var trakBox = Box(\"trak\", trakPayload); // Track Box\n        moovPayload = moovPayload.Concat(trakBox).ToArray();\n\n        var mvexPayload = GetMehd();\n        var trexBox = GetTrex();\n        mvexPayload = mvexPayload.Concat(trexBox).ToArray();\n\n        var mvexBox = Box(\"mvex\", mvexPayload); // Movie Extends Box\n        moovPayload = moovPayload.Concat(mvexBox).ToArray();\n\n        if (IsProtection)\n        {\n            var psshBox1 = GenPsshBoxForPlayReady();\n            var psshBox2 = GenPsshBoxForWideVine();\n            moovPayload = moovPayload.Concat(psshBox1).Concat(psshBox2).ToArray();\n        }\n\n        var moovBox = Box(\"moov\", moovPayload); // Movie Box\n\n        stream.Write(moovBox);\n\n        // var moofBox = GenMoof(); // Movie Extends Box\n        // stream.Write(moofBox);\n\n        return stream.ToArray();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/N_m3u8DL-RE.Parser.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>library</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <RootNamespace>N_m3u8DL_RE.Parser</RootNamespace>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <LangVersion>13.0</LangVersion>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\N_m3u8DL-RE.Common\\N_m3u8DL-RE.Common.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Parser.Processor;\n\npublic abstract class ContentProcessor\n{\n    public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig);\n    public abstract string Process(string rawText, ParserConfig parserConfig);\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Parser.Processor.DASH;\n\n/// <summary>\n/// MPD自动补充Namespace\n/// </summary>\npublic class DefaultDASHContentProcessor : ContentProcessor\n{\n    private static readonly Dictionary<string, string> NamespaceMap = new()\n    {\n        [\"cenc\"] = \"urn:mpeg:cenc:2013\",\n        [\"mspr\"] = \"urn:microsoft:playready\",\n        [\"mas\"] = \"urn:marlin:mas:1-0:services:schemas:mpd\",\n    };\n    \n    public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig)\n    {\n        if (extractorType != ExtractorType.MPEG_DASH) return false;\n\n        return NamespaceMap.Keys.Any(x => IsMissingNs(mpdContent, x));\n    }\n\n    public override string Process(string mpdContent, ParserConfig parserConfig)\n    {\n        Logger.InfoMarkUp(\"[gray]Namespace missing, try fix...[/]\");\n        var missingNamespaceKeys = NamespaceMap.Keys.Where(x => IsMissingNs(mpdContent, x)).ToList();\n        if (missingNamespaceKeys.Count == 0)\n            return mpdContent;\n        \n        var missingNamespaceDfns = missingNamespaceKeys.Select(key => $\"xmlns:{key}=\\\"{NamespaceMap[key]}\\\"\");\n        var declarations = string.Join(\" \", missingNamespaceDfns);\n        return ReplaceFirst(mpdContent, \"<MPD \", $\"<MPD {declarations} \");\n    }\n    \n    private static bool IsMissingNs(string rawText, string tag)\n    {\n        return !rawText.Contains($\"xmlns:{tag}\") && rawText.Contains($\"<{tag}:\");\n    }\n\n    // 替换字符串中第一次出现的指定子字符串。\n    private static string ReplaceFirst(string source, string oldValue, string newValue)\n    {\n        var index = source.IndexOf(oldValue, StringComparison.Ordinal);\n        return index < 0 ? source :\n            source.Remove(index, oldValue.Length).Insert(index, newValue);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/DefaultUrlProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Parser.Config;\nusing System.Web;\n\nnamespace N_m3u8DL_RE.Parser.Processor;\n\npublic class DefaultUrlProcessor : UrlProcessor\n{\n    public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams;\n\n    public override string Process(string oriUrl, ParserConfig paserConfig)\n    {\n        if (!oriUrl.StartsWith(\"http\")) return oriUrl;\n        \n        var uriFromConfig = new Uri(paserConfig.Url);\n        var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);\n\n        var oldUri = new Uri(oriUrl);\n        var newQuery = HttpUtility.ParseQueryString(oldUri.Query);\n        foreach (var item in uriFromConfigQuery.AllKeys)\n        {\n            if (newQuery.AllKeys.Contains(item))\n                newQuery.Set(item, uriFromConfigQuery.Get(item));\n            else\n                newQuery.Add(item, uriFromConfigQuery.Get(item));\n        }\n\n        if (string.IsNullOrEmpty(newQuery.ToString())) return oriUrl;\n        \n        Logger.Debug(\"Before: \" + oriUrl);\n        oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + \"?\" + newQuery).TrimEnd('?');\n        Logger.Debug(\"After: \" + oriUrl);\n\n        return oriUrl;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Constants;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Parser.Processor.HLS;\n\npublic partial class DefaultHLSContentProcessor : ContentProcessor\n{\n    [GeneratedRegex(\"#EXT-X-DISCONTINUITY\\\\s+#EXT-X-MAP:URI=\\\\\\\"(.*?)\\\\\\\",BYTERANGE=\\\\\\\"(.*?)\\\\\\\"\")]\n    private static partial Regex YkDVRegex();\n    [GeneratedRegex(\"#EXT-X-MAP:URI=\\\\\\\".*?BUMPER/[\\\\s\\\\S]+?#EXT-X-DISCONTINUITY\")]\n    private static partial Regex DNSPRegex();\n    [GeneratedRegex(@\"#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY\")]\n    private static partial Regex DNSPSubRegex();\n    [GeneratedRegex(\"(#EXTINF.*)(\\\\s+)(#EXT-X-KEY.*)\")]\n    private static partial Regex OrderFixRegex();\n    [GeneratedRegex(@\"#EXT-X-MAP.*\\.apple\\.com/\")]\n    private static partial Regex ATVRegex();\n    [GeneratedRegex(@\"(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)\")]\n    private static partial Regex ATVRegex2();\n\n    public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;\n\n    public override string Process(string m3u8Content, ParserConfig parserConfig)\n    {\n        // 处理content以\\r作为换行符的情况\n        if (m3u8Content.Contains('\\r') && !m3u8Content.Contains('\\n'))\n        {\n            m3u8Content = m3u8Content.Replace(\"\\r\", Environment.NewLine);\n        }\n\n        var m3u8Url = parserConfig.Url;\n        // YSP回放\n        if (m3u8Url.Contains(\"tlivecloud-playback-cdn.ysp.cctv.cn\") && m3u8Url.Contains(\"endtime=\"))\n        {\n            m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;\n        }\n\n        // IMOOC\n        if (m3u8Url.Contains(\"imooc.com/\"))\n        {\n            // M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);\n        }\n\n        // 针对YK #EXT-X-VERSION:7杜比视界片源修正\n        if (m3u8Content.Contains(\"#EXT-X-DISCONTINUITY\") && m3u8Content.Contains(\"#EXT-X-MAP\") && m3u8Content.Contains(\"ott.cibntv.net\") && m3u8Content.Contains(\"ccode=\"))\n        {\n            var ykmap = YkDVRegex();\n            foreach (Match m in ykmap.Matches(m3u8Content))\n            {\n                m3u8Content = m3u8Content.Replace(m.Value, $\"#EXTINF:0.000000,\\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\\n{m.Groups[1].Value}\");\n            }\n        }\n\n        // 针对Disney+修正\n        if (m3u8Content.Contains(\"#EXT-X-DISCONTINUITY\") && m3u8Content.Contains(\"#EXT-X-MAP\") && m3u8Url.Contains(\"media.dssott.com/\"))\n        {\n            Regex ykmap = DNSPRegex();\n            if (ykmap.IsMatch(m3u8Content))\n            {\n                m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, \"#XXX\");\n            }\n        }\n\n        // 针对Disney+字幕修正\n        if (m3u8Content.Contains(\"#EXT-X-DISCONTINUITY\") && m3u8Content.Contains(\"seg_00000.vtt\") && m3u8Url.Contains(\"media.dssott.com/\"))\n        {\n            Regex ykmap = DNSPSubRegex();\n            if (ykmap.IsMatch(m3u8Content))\n            {\n                m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, \"#XXX\");\n            }\n        }\n\n        // 针对AppleTv修正\n        if (m3u8Content.Contains(\"#EXT-X-DISCONTINUITY\") && m3u8Content.Contains(\"#EXT-X-MAP\") && (m3u8Url.Contains(\".apple.com/\") || ATVRegex().IsMatch(m3u8Content)))\n        {\n            // 只取加密部分即可\n            Regex ykmap = ATVRegex2();\n            if (ykmap.IsMatch(m3u8Content))\n            {\n                m3u8Content = \"#EXTM3U\\r\\n\" + ykmap.Match(m3u8Content).Groups[1].Value + \"\\r\\n#EXT-X-ENDLIST\";\n            }\n        }\n\n        // 修复#EXT-X-KEY与#EXTINF出现次序异常问题\n        var regex = OrderFixRegex();\n        if (regex.IsMatch(m3u8Content))\n        {\n            m3u8Content = regex.Replace(m3u8Content, \"$3$2$1\");\n        }\n\n        return m3u8Content;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Util;\nusing Spectre.Console;\n\nnamespace N_m3u8DL_RE.Parser.Processor.HLS;\n\npublic class DefaultHLSKeyProcessor : KeyProcessor\n{\n    public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS;\n\n\n    public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)\n    {\n        var iv = ParserUtil.GetAttribute(keyLine, \"IV\");\n        var method = ParserUtil.GetAttribute(keyLine, \"METHOD\");\n        var uri = ParserUtil.GetAttribute(keyLine, \"URI\");\n\n        Logger.Debug(\"METHOD:{},URI:{},IV:{}\", method, uri, iv);\n\n        var encryptInfo = new EncryptInfo(method);\n\n        // IV\n        if (!string.IsNullOrEmpty(iv))\n        {\n            encryptInfo.IV = HexUtil.HexToBytes(iv);\n        }\n        // 自定义IV\n        if (parserConfig.CustomeIV is { Length: > 0 }) \n        {\n            encryptInfo.IV = parserConfig.CustomeIV;\n        }\n\n        // KEY\n        try\n        {\n            if (parserConfig.CustomeKey is { Length: > 0 })\n            {\n                encryptInfo.Key = parserConfig.CustomeKey;\n            }\n            else if (uri.ToLower().StartsWith(\"base64:\"))\n            {\n                encryptInfo.Key = Convert.FromBase64String(uri[7..]);\n            }\n            else if (uri.ToLower().StartsWith(\"data:;base64,\"))\n            {\n                encryptInfo.Key = Convert.FromBase64String(uri[13..]);\n            }\n            else if (uri.ToLower().StartsWith(\"data:text/plain;base64,\"))\n            {\n                encryptInfo.Key = Convert.FromBase64String(uri[23..]);\n            }\n            else if (File.Exists(uri))\n            {\n                encryptInfo.Key = File.ReadAllBytes(uri);\n            }\n            else if (!string.IsNullOrEmpty(uri))\n            {\n                var retryCount = parserConfig.KeyRetryCount;\n                var segUrl = PreProcessUrl(ParserUtil.CombineURL(m3u8Url, uri), parserConfig);\n                getHttpKey:\n                try\n                {\n                    var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;\n                    encryptInfo.Key = bytes;\n                }\n                catch (Exception _ex) when (!_ex.Message.Contains(\"scheme is not supported.\"))\n                {\n                    Logger.WarnMarkUp($\"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]\");\n                    Thread.Sleep(1000);\n                    if (retryCount-- > 0) goto getHttpKey;\n                    throw;\n                }\n            }\n        }\n        catch (Exception ex)\n        {\n            Logger.Error(ResString.cmd_loadKeyFailed + \": \" + ex.Message);\n            encryptInfo.Method = EncryptMethod.UNKNOWN;\n        }\n\n        if (parserConfig.CustomMethod == null) return encryptInfo;\n        \n        // 处理自定义加密方式\n        encryptInfo.Method = parserConfig.CustomMethod.Value;\n        Logger.Warn(\"METHOD changed from {} to {}\", method, encryptInfo.Method);\n\n        return encryptInfo;\n    }\n\n    /// <summary>\n    /// 预处理URL\n    /// </summary>\n    private string PreProcessUrl(string url, ParserConfig parserConfig)\n    {\n        foreach (var p in parserConfig.UrlProcessors)\n        {\n            if (p.CanProcess(ExtractorType.HLS, url, parserConfig))\n            {\n                url = p.Process(url, parserConfig);\n            }\n        }\n\n        return url;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Parser.Processor;\n\npublic abstract class KeyProcessor\n{\n    public abstract bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);\n    public abstract EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs",
    "content": "﻿using N_m3u8DL_RE.Common.Enum;\nusing N_m3u8DL_RE.Parser.Config;\n\nnamespace N_m3u8DL_RE.Parser.Processor;\n\npublic abstract class UrlProcessor\n{\n    public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig);\n    public abstract string Process(string oriUrl, ParserConfig parserConfig);\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/StreamExtractor.cs",
    "content": "﻿using System.Diagnostics.CodeAnalysis;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Common.Entity;\nusing N_m3u8DL_RE.Common.Log;\nusing N_m3u8DL_RE.Common.Resource;\nusing N_m3u8DL_RE.Parser.Constants;\nusing N_m3u8DL_RE.Parser.Extractor;\nusing N_m3u8DL_RE.Common.Util;\nusing N_m3u8DL_RE.Common.Enum;\n\nnamespace N_m3u8DL_RE.Parser;\n\npublic class StreamExtractor\n{\n    public ExtractorType ExtractorType => extractor.ExtractorType;\n    private IExtractor extractor;\n    private ParserConfig parserConfig = new();\n    private string rawText;\n    private static SemaphoreSlim semaphore = new(1, 1);\n\n    public Dictionary<string, string> RawFiles { get; set; } = new(); // 存储（文件名,文件内容）\n\n    public StreamExtractor(ParserConfig parserConfig)\n    {\n        this.parserConfig = parserConfig;\n    }\n\n    public async Task LoadSourceFromUrlAsync(string url)\n    {\n        Logger.Info(ResString.loadingUrl + url);\n        if (url.StartsWith(\"file:\"))\n        {\n            var uri = new Uri(url);\n            this.rawText = await File.ReadAllTextAsync(uri.LocalPath);\n            parserConfig.OriginalUrl = parserConfig.Url = url;\n        }\n        else if (url.StartsWith(\"http\"))\n        {\n            parserConfig.OriginalUrl = url;\n            (this.rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers);\n            parserConfig.Url = url;\n        }\n        else if (File.Exists(url))\n        {\n            url = Path.GetFullPath(url);\n            this.rawText = await File.ReadAllTextAsync(url);\n            parserConfig.OriginalUrl = parserConfig.Url = new Uri(url).AbsoluteUri;\n        }\n\n        if (string.IsNullOrWhiteSpace(rawText))\n        {\n            throw new Exception(ResString.loadUrlFailed);\n        }\n        \n        this.rawText = rawText.Trim();\n        LoadSourceFromText(this.rawText);\n    }\n\n    [MemberNotNull(nameof(this.rawText), nameof(this.extractor))]\n    private void LoadSourceFromText(string rawText)\n    {\n        var rawType = \"txt\";\n        rawText = rawText.Trim();\n        this.rawText = rawText;\n        if (rawText.StartsWith(HLSTags.ext_m3u))\n        {\n            Logger.InfoMarkUp(ResString.matchHLS);\n            extractor = new HLSExtractor(parserConfig);\n            rawType = \"m3u8\";\n        }\n        else if (rawText.Contains(\"</MPD>\") && rawText.Contains(\"<MPD\"))\n        {\n            Logger.InfoMarkUp(ResString.matchDASH);\n            // extractor = new DASHExtractor(parserConfig);\n            extractor = new DASHExtractor2(parserConfig);\n            rawType = \"mpd\";\n        }\n        else if (rawText.Contains(\"</SmoothStreamingMedia>\") && rawText.Contains(\"<SmoothStreamingMedia\"))\n        {\n            Logger.InfoMarkUp(ResString.matchMSS);\n            // extractor = new DASHExtractor(parserConfig);\n            extractor = new MSSExtractor(parserConfig);\n            rawType = \"ism\";\n        }\n        else if (rawText == ResString.ReLiveTs)\n        {\n            Logger.InfoMarkUp(ResString.matchTS);\n            extractor = new LiveTSExtractor(parserConfig);\n        }\n        else if (rawText == ResString.ReBinaryData)\n        {\n            Logger.InfoMarkUp(ResString.matchBinaryData);\n            throw new NotSupportedException(ResString.notSupported);\n        }\n        else\n        {\n            throw new NotSupportedException(ResString.notSupported);\n        }\n\n        RawFiles[$\"raw.{rawType}\"] = rawText;\n    }\n\n    /// <summary>\n    /// 开始解析流媒体信息\n    /// </summary>\n    /// <returns></returns>\n    public async Task<List<StreamSpec>> ExtractStreamsAsync()\n    {\n        try\n        {\n            await semaphore.WaitAsync();\n            Logger.Info(ResString.parsingStream);\n            return await extractor.ExtractStreamsAsync(rawText);\n        }\n        finally\n        {\n            semaphore.Release();\n        }\n    }\n\n    /// <summary>\n    /// 根据规格说明填充媒体播放列表信息\n    /// </summary>\n    /// <param name=\"streamSpecs\"></param>\n    public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        try\n        {\n            await semaphore.WaitAsync();\n            Logger.Info(ResString.parsingStream);\n            await extractor.FetchPlayListAsync(streamSpecs);\n        }\n        finally\n        {\n            semaphore.Release();\n        }\n    }\n\n    public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)\n    {\n        try\n        {\n            await semaphore.WaitAsync();\n            await RetryUtil.WebRequestRetryAsync(async () =>\n            {\n                await extractor.RefreshPlayListAsync(streamSpecs);\n                return true;\n            }, retryDelayMilliseconds: 1000, maxRetries: 5);\n        }\n        finally\n        {\n            semaphore.Release();\n        }\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Parser/Util/ParserUtil.cs",
    "content": "﻿using N_m3u8DL_RE.Parser.Constants;\nusing System.Text.RegularExpressions;\n\nnamespace N_m3u8DL_RE.Parser.Util;\n\npublic static partial class ParserUtil\n{\n    [GeneratedRegex(@\"\\$Number%([^$]+)d\\$\")]\n    private static partial Regex VarsNumberRegex();\n\n    /// <summary>\n    /// 从以下文本中获取参数\n    /// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS=\"mp4a.40.2,avc1.64001f\",RESOLUTION=1280x720,NAME=\"720\"\n    /// </summary>\n    /// <param name=\"line\">等待被解析的一行文本</param>\n    /// <param name=\"key\">留空则获取第一个英文冒号后的全部字符</param>\n    /// <returns></returns>\n    public static string? GetAttribute(string line, string key = \"\")\n    {\n        line = line.Trim();\n        if (key == \"\")\n            return line[(line.IndexOf(':') + 1)..];\n\n        int index;\n        string? result = null;\n        if ((index = line.IndexOf(key + \"=\\\"\", StringComparison.Ordinal)) > -1)\n        {\n            var startIndex = index + (key + \"=\\\"\").Length;\n            var endIndex = startIndex + line[startIndex..].IndexOf('\\\"');\n            result = line[startIndex..endIndex];\n        }\n        else if ((index = line.IndexOf(key + \"=\", StringComparison.Ordinal)) > -1)\n        {\n            var startIndex = index + (key + \"=\").Length;\n            var endIndex = startIndex + line[startIndex..].IndexOf(',');\n            result = endIndex >= startIndex ? line[startIndex..endIndex] : line[startIndex..];\n        }\n\n        return result;\n    }\n\n    /// <summary>\n    /// 从如下文本中提取\n    /// <n>[@<o>]\n    /// </summary>\n    /// <param name=\"input\"></param>\n    /// <returns>n(length) o(start)</returns>\n    public static (long, long?) GetRange(string input)\n    {\n        var t = input.Split('@');\n        return t.Length switch\n        {\n            <= 0 => (0, null),\n            1 => (Convert.ToInt64(t[0]), null),\n            2 => (Convert.ToInt64(t[0]), Convert.ToInt64(t[1])),\n            _ => (0, null)\n        };\n    }\n\n    /// <summary>\n    /// 从100-300这种字符串中获取StartRange, ExpectLength信息\n    /// </summary>\n    /// <param name=\"range\"></param>\n    /// <returns>StartRange, ExpectLength</returns>\n    public static (long, long) ParseRange(string range)\n    {\n        var start = Convert.ToInt64(range.Split('-')[0]);\n        var end = Convert.ToInt64(range.Split('-')[1]);\n        return (start, end - start + 1);\n    }\n\n    /// <summary>\n    /// MPD SegmentTemplate替换\n    /// </summary>\n    /// <param name=\"text\"></param>\n    /// <param name=\"keyValuePairs\"></param>\n    /// <returns></returns>\n    public static string ReplaceVars(string text, Dictionary<string, object?> keyValuePairs)\n    {\n        foreach (var item in keyValuePairs)\n            if (text.Contains(item.Key))\n                text = text.Replace(item.Key, item.Value!.ToString());\n\n        // 处理特殊形式数字 如 $Number%05d$\n        var regex = VarsNumberRegex();\n        if (regex.IsMatch(text) && keyValuePairs.TryGetValue(DASHTags.TemplateNumber, out var keyValuePair)) \n        {\n            foreach (Match m in regex.Matches(text))\n            {\n                text = text.Replace(m.Value, keyValuePair?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));\n            }\n        }\n\n        return text;\n    }\n\n    /// <summary>\n    /// 拼接Baseurl和RelativeUrl\n    /// </summary>\n    /// <param name=\"baseurl\">Baseurl</param>\n    /// <param name=\"url\">RelativeUrl</param>\n    /// <returns></returns>\n    public static string CombineURL(string baseurl, string url)\n    {\n        if (string.IsNullOrEmpty(baseurl))\n            return url;\n\n        var uri1 = new Uri(baseurl);  // 这里直接传完整的URL即可\n        var uri2 = new Uri(uri1, url);\n        url = uri2.ToString();\n\n        return url;\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/Common/Util/HexUtilTests.cs",
    "content": "using N_m3u8DL_RE.Common.Util;\n\nnamespace N_m3u8DL_RE.Tests.Common.Util;\n\npublic class HexUtilTests\n{\n    [Fact]\n    public void BytesToHex_MultipleBytesWithDefaultSplit_ReturnsHexChars()\n    {\n        var result = HexUtil.BytesToHex([0xAB, 0xCD, 0xEF]);\n        Assert.Equal(\"ABCDEF\", result);\n    }\n\n    [Fact]\n    public void BytesToHex_MultipleBytesWithCustomSplit_ReturnsHexChars()\n    {\n        var result = HexUtil.BytesToHex([0xAA, 0xBB, 0xCC], \":\");\n        Assert.Equal(\"AA:BB:CC\", result);\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/N_m3u8DL-RE.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFramework>net10.0</TargetFramework>\n        <RootNamespace>N_m3u8DL_RE.Tests</RootNamespace>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n\n        <IsPackable>false</IsPackable>\n        <IsTestProject>true</IsTestProject>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"coverlet.collector\" Version=\"6.0.0\"/>\n        <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.8.0\"/>\n        <PackageReference Include=\"Shouldly\" Version=\"4.3.0\" />\n        <PackageReference Include=\"xunit\" Version=\"2.5.3\"/>\n        <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.5.3\"/>\n    </ItemGroup>\n\n    <ItemGroup>\n        <Using Include=\"Xunit\"/>\n    </ItemGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\N_m3u8DL-RE.Parser\\N_m3u8DL-RE.Parser.csproj\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <EmbeddedResource Include=\"Resources\\**\\*.mpd\"/>\n        <EmbeddedResource Include=\"Resources\\**\\*.m3u8\"/>\n        <EmbeddedResource Include=\"Resources\\**\\*.ism\"/>\n    </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/Parser/Extractor/DASHExtractor2Tests.cs",
    "content": "using Shouldly;\nusing N_m3u8DL_RE.Parser.Config;\nusing N_m3u8DL_RE.Parser.Extractor;\n\nnamespace N_m3u8DL_RE.Tests.Parser.Extractor;\n\npublic class DASHExtractor2Tests\n{\n    private static ParserConfig CreateTestConfig(string mpdFileName) => new ParserConfig\n    {\n        OriginalUrl = $\"file:///fake/path/to/{mpdFileName}\",\n    };\n    \n    [Fact]\n    public async Task DASHExtractor2_Normal()\n    {\n        const string mpdName = \"Dash.Manifest_1080p.mpd\";\n        var config = CreateTestConfig(mpdName);\n        var content = ResourceHelper.Read(mpdName);\n        var extractor = new DASHExtractor2(config);\n        var results = await extractor.ExtractStreamsAsync(content);\n        results.ShouldNotBeNull();\n        results.Count.ShouldBe(23);\n\n        var first = results.First();\n        first.ToString().ShouldBe(\"[aqua]Vid[/] 512x288 | 386 Kbps | 1 | avc1.64001f | 184 Segments | Main | ~12m16s\");\n        first.AudioId.ShouldBe(\"15\");\n        first.Bandwidth.ShouldBe(386437);\n        first.Extension.ShouldBe(\"m4s\");\n        first.Language.ShouldBe(\"und\");\n        first.SubtitleId.ShouldBe(\"25\");\n        first.Playlist.ShouldNotBeNull();\n        first.Playlist.IsLive.ShouldBe(false);\n        first.Playlist.TotalDuration.ShouldBe(736);\n        first.Playlist.MediaInit.ShouldNotBeNull();\n        first.Playlist.MediaInit.Url.ShouldBe(\"1/init.mp4\");\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/ResourceHelper.cs",
    "content": "using System.Reflection;\n\nnamespace N_m3u8DL_RE.Tests;\n\npublic static class ResourceHelper\n{\n    private static readonly Assembly _assembly = Assembly.GetExecutingAssembly();\n    private const string ResourcePrefix = \"N_m3u8DL_RE.Tests.Resources.\";\n\n    public static string Read(string fileName)\n    {\n        var resourceName = ResourcePrefix + fileName;\n        using var stream = _assembly.GetManifestResourceStream(resourceName)\n                           ?? throw new ArgumentException($\"Embedded resource not found: {resourceName}\", nameof(fileName));\n        \n        using var reader = new StreamReader(stream);\n        return reader.ReadToEnd();\n    }\n}"
  },
  {
    "path": "src/N_m3u8DL-RE.Tests/Resources/Dash/Manifest_1080p.mpd",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\nVersion information:\nAxinom.MediaProcessing v3.0.0 targeting General Purpose Media Format specification v7\nffmpeg version N-81423-g61fac0e Copyright (c) 2000-2016 the FFmpeg developers\nx265 [info]: HEVC encoder version 2.0+12-49a0d1176aef5bc6\nx264 0.148.2705 3f5ed56\nMP4Box - GPAC version 0.6.2-DEV-rev683-g7b29fbe-master\nMediaInfoLib - v0.7.87\n\nFor more info about this video, see https://github.com/Axinom/dash-test-vectors\n-->\n<MPD xmlns=\"urn:mpeg:dash:schema:mpd:2011\" minBufferTime=\"PT1.500S\" type=\"static\" mediaPresentationDuration=\"PT0H12M14.000S\" maxSegmentDuration=\"PT0H0M4.000S\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash264\">\n\t<Period duration=\"PT0H12M14.000S\">\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"1\" maxWidth=\"1920\" maxHeight=\"1080\" maxFrameRate=\"24\" par=\"16:9\" lang=\"und\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"main\" />\n\t\t\t<SegmentTemplate timescale=\"24\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"96\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"1\" mimeType=\"video/mp4\" codecs=\"avc1.64001f\" width=\"512\" height=\"288\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"386437\"></Representation>\n\t\t\t<Representation id=\"2\" mimeType=\"video/mp4\" codecs=\"avc1.64001f\" width=\"640\" height=\"360\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"761570\"></Representation>\n\t\t\t<Representation id=\"3\" mimeType=\"video/mp4\" codecs=\"avc1.640028\" width=\"852\" height=\"480\" frameRate=\"24\" sar=\"640:639\" startWithSAP=\"1\" bandwidth=\"1117074\"></Representation>\n\t\t\t<Representation id=\"4\" mimeType=\"video/mp4\" codecs=\"avc1.640032\" width=\"1280\" height=\"720\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"1941893\"></Representation>\n\t\t\t<Representation id=\"5\" mimeType=\"video/mp4\" codecs=\"avc1.640033\" width=\"1920\" height=\"1080\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"2723012\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"1\" maxWidth=\"1920\" maxHeight=\"1080\" maxFrameRate=\"24\" par=\"16:9\" lang=\"und\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"main\" />\n\t\t\t<SegmentTemplate timescale=\"1200000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4799983\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"8\" mimeType=\"video/mp4\" codecs=\"hev1.2.4.L63.90\" width=\"512\" height=\"288\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"395735\"></Representation>\n\t\t\t<Representation id=\"9\" mimeType=\"video/mp4\" codecs=\"hev1.2.4.L63.90\" width=\"640\" height=\"360\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"689212\"></Representation>\n\t\t\t<Representation id=\"10\" mimeType=\"video/mp4\" codecs=\"hev1.2.4.L90.90\" width=\"852\" height=\"480\" frameRate=\"24\" sar=\"640:639\" startWithSAP=\"1\" bandwidth=\"885518\"></Representation>\n\t\t\t<Representation id=\"11\" mimeType=\"video/mp4\" codecs=\"hev1.2.4.L93.90\" width=\"1280\" height=\"720\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"1474186\"></Representation>\n\t\t\t<Representation id=\"12\" mimeType=\"video/mp4\" codecs=\"hev1.2.4.L120.90\" width=\"1920\" height=\"1080\" frameRate=\"24\" sar=\"1:1\" startWithSAP=\"1\" bandwidth=\"1967542\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"2\" lang=\"en\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"main\" />\n\t\t\t<SegmentTemplate timescale=\"24000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"95232\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"15\" mimeType=\"audio/mp4\" codecs=\"mp4a.40.29\" audioSamplingRate=\"48000\" startWithSAP=\"1\" bandwidth=\"131351\">\n\t\t\t\t<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"2\" />\n\t\t\t</Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"2\" lang=\"en-AU\">\n\t\t\t<SegmentTemplate timescale=\"24000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"95232\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"16\" mimeType=\"audio/mp4\" codecs=\"mp4a.40.29\" audioSamplingRate=\"48000\" startWithSAP=\"1\" bandwidth=\"130991\">\n\t\t\t\t<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"2\" />\n\t\t\t</Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"2\" lang=\"et-ET\">\n\t\t\t<SegmentTemplate timescale=\"24000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"95232\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"17\" mimeType=\"audio/mp4\" codecs=\"mp4a.40.29\" audioSamplingRate=\"48000\" startWithSAP=\"1\" bandwidth=\"130749\">\n\t\t\t\t<AudioChannelConfiguration schemeIdUri=\"urn:mpeg:dash:23003:3:audio_channel_configuration:2011\" value=\"2\" />\n\t\t\t</Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"en\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"main\" />\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"18\" mimeType=\"application/mp4\" codecs=\"wvtt\" startWithSAP=\"1\" bandwidth=\"428\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"en\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"main\" />\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"19\" mimeType=\"application/mp4\" codecs=\"stpp\" startWithSAP=\"1\" bandwidth=\"1095\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"zh-Hant-CN\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"20\" mimeType=\"application/mp4\" codecs=\"wvtt\" startWithSAP=\"1\" bandwidth=\"429\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"zh-Hant-CN\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"21\" mimeType=\"application/mp4\" codecs=\"stpp\" startWithSAP=\"1\" bandwidth=\"1101\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"nl\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"22\" mimeType=\"application/mp4\" codecs=\"wvtt\" startWithSAP=\"1\" bandwidth=\"429\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"nl\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"23\" mimeType=\"application/mp4\" codecs=\"stpp\" startWithSAP=\"1\" bandwidth=\"1098\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"ru\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"24\" mimeType=\"application/mp4\" codecs=\"wvtt\" startWithSAP=\"1\" bandwidth=\"445\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"ru\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"25\" mimeType=\"application/mp4\" codecs=\"stpp\" startWithSAP=\"1\" bandwidth=\"1142\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"es\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"26\" mimeType=\"application/mp4\" codecs=\"wvtt\" startWithSAP=\"1\" bandwidth=\"430\"></Representation>\n\t\t</AdaptationSet>\n\t\t<AdaptationSet segmentAlignment=\"true\" group=\"3\" lang=\"es\">\n\t\t\t<Role schemeIdUri=\"urn:mpeg:dash:role:2011\" value=\"subtitle\" />\n\t\t\t<SegmentTemplate timescale=\"1000\" media=\"$RepresentationID$/$Number%04d$.m4s\" startNumber=\"1\" duration=\"4000\" initialization=\"$RepresentationID$/init.mp4\" />\n\t\t\t<Representation id=\"27\" mimeType=\"application/mp4\" codecs=\"stpp\" startWithSAP=\"1\" bandwidth=\"1102\"></Representation>\n\t\t</AdaptationSet>\n\t</Period>\n</MPD>"
  },
  {
    "path": "src/N_m3u8DL-RE.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.3.32505.426\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"N_m3u8DL-RE\", \"N_m3u8DL-RE\\N_m3u8DL-RE.csproj\", \"{E6915BF9-8306-4F62-B357-23430F0D80B5}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"N_m3u8DL-RE.Common\", \"N_m3u8DL-RE.Common\\N_m3u8DL-RE.Common.csproj\", \"{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}\"\nEndProject\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"N_m3u8DL-RE.Parser\", \"N_m3u8DL-RE.Parser\\N_m3u8DL-RE.Parser.csproj\", \"{0DA02925-AF3A-4598-AF01-91AE5539FCA1}\"\nEndProject\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"N_m3u8DL-RE.Tests\", \"N_m3u8DL-RE.Tests\\N_m3u8DL-RE.Tests.csproj\", \"{CF51B8FF-E4EF-49D6-8595-AFAFD96D9253}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{E6915BF9-8306-4F62-B357-23430F0D80B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{E6915BF9-8306-4F62-B357-23430F0D80B5}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{E6915BF9-8306-4F62-B357-23430F0D80B5}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{E6915BF9-8306-4F62-B357-23430F0D80B5}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{18D8BC30-512C-4A07-BD4C-E96DF5CF6341}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Release|Any CPU.Build.0 = Release|Any CPU\n\t\t{CF51B8FF-E4EF-49D6-8595-AFAFD96D9253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{CF51B8FF-E4EF-49D6-8595-AFAFD96D9253}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{CF51B8FF-E4EF-49D6-8595-AFAFD96D9253}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{CF51B8FF-E4EF-49D6-8595-AFAFD96D9253}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tRESX_NeutralResourcesLanguage = en-US\n\t\tSolutionGuid = {87F963D4-EA06-413D-9372-C726711C32B5}\n\tEndGlobalSection\nEndGlobal\n"
  }
]