Compare commits

...

44 Commits

Author SHA1 Message Date
√(noham)²
948f7aa75a Update MSSMoovProcessor.cs 2025-02-01 19:13:42 +01:00
lollolong
cd4dfb5e75
add: role ForcedSubtitle, useful for filtering (#574)
see also https://github.com/google/ExoPlayer/issues/9727
2024-12-29 21:45:24 +08:00
nilaoda
f47a0722cf 优化Init文件加密判断 (#562)
兼容了非标准的m3u8, 详情请看issue #550
2024-12-18 19:57:24 +08:00
nilaoda
ea9de55eac
更新编译日期 2024-12-03 11:17:06 +08:00
nilaoda
87914a30cc
恢复展示速度单位 Fix #544 2024-12-03 11:02:32 +08:00
nilaoda
e350ab7233
更新流水线文件名规则 (#541) 2024-12-02 01:27:26 +08:00
nilaoda
4c4617b780 v0.3.0 2024-12-01 23:19:16 +08:00
nilaoda
7a54c54786 文件名中包含版本信息 2024-12-01 23:17:22 +08:00
nilaoda
9f530f2cf6
修正fMP4录制文件无法播放问题 (#537) 2024-12-01 23:12:51 +08:00
nilaoda
a8646eb7e7 修复KID寻找异常问题 Fix #529 2024-12-01 12:17:56 +08:00
nilaoda
800ce3d615 v0.2.3 2024-12-01 11:54:15 +08:00
nilaoda
49d37c9f14 修复国际化问题 (#532) 2024-12-01 11:41:00 +08:00
nilaoda
117c73f54b 修正tar压缩命令 2024-12-01 01:20:12 +08:00
nilaoda
b044cdb305 v0.2.2 2024-12-01 00:35:48 +08:00
nilaoda
adbe376ae0 优化部分代码 2024-12-01 00:30:53 +08:00
nilaoda
cacf9b0ff0
增加第三方工具的下载地址提示 (#525) 2024-11-30 22:28:44 +08:00
nilaoda
bc8b5a92a9 去除多余判断 2024-11-30 22:22:15 +08:00
nilaoda
7463915fbb 优化构建发布流程 2024-11-30 22:21:57 +08:00
nilaoda
8702d36276 解决mp4decrypt解密中文文件名失败问题 (#524) 2024-11-30 00:36:17 +08:00
nilaoda
3081701a32
使用tput替代stty (#523) 2024-11-29 20:57:20 +08:00
nilaoda
77fdaaf9bd
恢复默认使用ffmpeg解密 (#522) 2024-11-29 20:52:23 +08:00
nilaoda
8095c6e172
必要时才调用mp4解密 (#519) 2024-11-29 19:04:47 +08:00
nilaoda
6bd906e4e6
增加 linux-bionic 编译 (#518) 2024-11-29 19:03:25 +08:00
nilaoda
09d9f0e320 修正ShakaPackager枚举值 2024-11-29 00:48:01 +08:00
nilaoda
9752df8aae
增加 linux-musl 编译 (#515)
注意: musl构建版本不支持全球化
2024-11-28 22:09:46 +08:00
nilaoda
b3d95963db
无法识别langCode时置为und (#508) 2024-11-24 18:38:03 +08:00
nilaoda
312325ca18 默认使用ffmpeg进行mp4解密 (#504)
BREAKING CHANGE
2024-11-24 00:45:54 +08:00
nilaoda
e8e92b6337
新增--allow-hls-multi-ext-map (#503) 2024-11-23 19:32:25 +08:00
nilaoda
0c73b730bb
支持设置http请求超时时间和设置禁止检测更新 (#502)
`--disable-update-check`
`--http-request-timeout`
2024-11-23 18:24:29 +08:00
nilaoda
c004a1c72f
适配WindowsVista编译 (#499) 2024-11-22 19:46:34 +08:00
nilaoda
bb20d50122 增加Windows7编译 (#494) 2024-11-21 01:30:28 +08:00
nilaoda
3cd3bb9516
修复MPD解析过慢问题 (#493) 2024-11-18 21:20:39 +08:00
nilaoda
5a56e34cd5 升级到.NET9 2024-11-17 19:51:29 +08:00
irodai-majom
f6ad09255e
dotnet 8.0 in libraries (#483) 2024-11-11 08:39:34 +08:00
nilaoda
b9d3b57b39 调整部分代码结构 2024-11-10 18:44:58 +08:00
fireattack
7d8e7c6402
Make sure ms part is 3-digit (#477) 2024-11-10 16:20:24 +08:00
irodai-majom
9fc37d5b61
Marked util classes as static (#460)
* Marked util classes as static

* Used file-scoped namespaces
2024-11-10 16:15:30 +08:00
nilaoda
8a25815c1f
支持只输入KEY未输入KID场景自动补全 (#476) 2024-11-02 20:44:00 +08:00
fireattack
dd30bd99f9
Fix WebVttSub not be able to parse string without ms (#473) 2024-11-02 15:29:21 +08:00
irodai-majom
9c49fce4ff
Optimized HexToBytes function (#469)
- used Span
- used Convert.FromHexString vectorized function
2024-10-30 22:50:42 +08:00
nilaoda
6e92acfda9 修复文件占用问题 2024-10-21 23:48:21 +08:00
nilaoda
d1ffac817d 更新README 2024-10-20 21:53:40 +08:00
nilaoda
164f2cb59b
支持TS格式混流 (#466) 2024-10-20 21:41:55 +08:00
nilaoda
2bf4f29f28
修复Gzip解压报错文件找不到问题 (#465) 2024-10-20 12:39:04 +08:00
98 changed files with 11062 additions and 11359 deletions

View File

@ -10,7 +10,7 @@ on:
required: false required: false
tag: tag:
type: string type: string
description: 'Release version tag (e.g. v1.2.3)' description: 'Release version tag (e.g. v0.2.1-beta)'
required: true required: true
ref: ref:
type: string type: string
@ -19,16 +19,73 @@ on:
default: 'main' default: 'main'
env: env:
DOTNET_SDK_VERSION: "8.0.*" DOTNET_SDK_VERSION: "9.0.*"
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
jobs: jobs:
build-win-x64-arm64: set-date:
runs-on: ubuntu-latest
outputs:
date: ${{ steps.get_date.outputs.date }}
tag: ${{ steps.format_tag.outputs.tag }}
steps:
- name: Get Date in UTC+8
id: get_date
run: |
DATE=$(date -u -d '8 hours' +'%Y%m%d')
echo "date=${DATE}" >> "$GITHUB_OUTPUT"
- name: Determine Tag
id: format_tag
run: |
if [ "${{ github.event.inputs.doRelease }}" == "true" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="actions-$GITHUB_RUN_ID"
fi
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
build-win-nt6_0-x86:
runs-on: windows-latest runs-on: windows-latest
needs: set-date
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Install zip
run: choco install zip --no-progress --yes
- name: Set up dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- run: powershell -Command "(Get-Content src/N_m3u8DL-RE/N_m3u8DL-RE.csproj) -replace '<TargetFramework>.*</TargetFramework>', '<TargetFramework>net9.0-windows</TargetFramework>' | Set-Content src/N_m3u8DL-RE/N_m3u8DL-RE.csproj"
- run: dotnet add src/N_m3u8DL-RE/N_m3u8DL-RE.csproj package YY-Thunks --version 1.1.4
- run: dotnet add src/N_m3u8DL-RE/N_m3u8DL-RE.csproj package VC-LTL --version 5.1.1
- run: dotnet publish src/N_m3u8DL-RE -p:TargetPlatformMinVersion=6.0 -r win-x86 -c Release -o artifact-x86
- name: Package [win-x86]
run: |
cd artifact-x86
zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-NT6.0-x86_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe
- name: Upload Artifact[win-x86]
uses: actions/upload-artifact@v3.1.3
with:
name: win-NT6.0-x86
path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-NT6.0-x86_${{ needs.set-date.outputs.date }}.zip
build-win-x64-arm64:
runs-on: windows-latest
needs: set-date
steps:
- uses: actions/checkout@v1
- name: Install zip
run: choco install zip --no-progress --yes
- name: Set up dotnet - name: Set up dotnet
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
@ -37,59 +94,169 @@ jobs:
- run: dotnet publish src/N_m3u8DL-RE -r win-x64 -c Release -o artifact-x64 - run: dotnet publish src/N_m3u8DL-RE -r win-x64 -c Release -o artifact-x64
- run: dotnet publish src/N_m3u8DL-RE -r win-arm64 -c Release -o artifact-arm64 - run: dotnet publish src/N_m3u8DL-RE -r win-arm64 -c Release -o artifact-arm64
- name: Upload Artifact[win-x64] - name: Package [win]
run: |
cd artifact-x64
zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-x64_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe
cd ../artifact-arm64
zip ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-arm64_${{ needs.set-date.outputs.date }}.zip N_m3u8DL-RE.exe
- name: Upload Artifact [win-x64]
uses: actions/upload-artifact@v3.1.3 uses: actions/upload-artifact@v3.1.3
with: with:
name: N_m3u8DL-RE_Beta_win-x64 name: win-x64
path: artifact-x64\N_m3u8DL-RE.exe path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-x64_${{ needs.set-date.outputs.date }}.zip
- name: Upload Artifact[win-arm64] - name: Upload Artifact [win-arm64]
uses: actions/upload-artifact@v3.1.3 uses: actions/upload-artifact@v3.1.3
with: with:
name: N_m3u8DL-RE_Beta_win-arm64 name: win-arm64
path: artifact-arm64\N_m3u8DL-RE.exe path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_win-arm64_${{ needs.set-date.outputs.date }}.zip
build-linux-x64: build-linux-x64-arm64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: ubuntu:18.04 needs: set-date
steps: steps:
- run: apt-get update # https://learn.microsoft.com/zh-cn/dotnet/core/deploying/native-aot/cross-compile
- run: apt-get install -y curl wget - run: |
sudo dpkg --add-architecture arm64
sudo bash -c 'cat > /etc/apt/sources.list.d/arm64.list <<EOF
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse
EOF'
sudo sed -i -e 's/deb http/deb [arch=amd64] http/g' /etc/apt/sources.list
sudo sed -i -e 's/deb mirror/deb [arch=amd64] mirror/g' /etc/apt/sources.list
sudo apt-get update
sudo apt-get install -y curl wget libicu-dev libcurl4-openssl-dev zlib1g-dev libkrb5-dev clang llvm binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu zlib1g-dev:arm64
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Set up dotnet - name: Set up dotnet
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }} dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- run: apt-get install -y libicu-dev libcurl4-openssl-dev zlib1g-dev libkrb5-dev
- run: dotnet publish src/N_m3u8DL-RE -r linux-x64 -c Release -o artifact - run: dotnet publish src/N_m3u8DL-RE -r linux-x64 -c Release -o artifact
- run: dotnet publish src/N_m3u8DL-RE -r linux-arm64 -c Release -o artifact-arm64
- name: Upload Artifact[linux-x64] - name: Package [linux]
run: |
cd artifact
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
cd ../artifact-arm64
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
- name: Upload Artifact [linux-x64]
uses: actions/upload-artifact@v3.1.3 uses: actions/upload-artifact@v3.1.3
with: with:
name: N_m3u8DL-RE_Beta_linux-x64 name: linux-x64
path: artifact/N_m3u8DL-RE path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-x64_${{ needs.set-date.outputs.date }}.tar.gz
build-linux-arm64:
runs-on: ubuntu-latest
container: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-18.04-cross-arm64-20220312201346-b2c2436
steps:
- uses: actions/checkout@v1
- name: Set up dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- run: dotnet publish src/N_m3u8DL-RE -r linux-arm64 -c Release -p:StripSymbols=true -p:CppCompilerAndLinker=clang-9 -p:SysRoot=/crossrootfs/arm64 -o artifact
- name: Upload Artifact[linux-arm64] - name: Upload Artifact[linux-arm64]
uses: actions/upload-artifact@v3.1.3 uses: actions/upload-artifact@v3.1.3
with: with:
name: N_m3u8DL-RE_Beta_linux-arm64 name: linux-arm64
path: artifact/N_m3u8DL-RE path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-arm64_${{ needs.set-date.outputs.date }}.tar.gz
build-android-bionic-x64-arm64:
runs-on: windows-latest
needs: set-date
steps:
- uses: actions/checkout@v1
- name: Set up NDK
shell: pwsh
run: |
Invoke-WebRequest -Uri "https://dl.google.com/android/repository/android-ndk-r27c-windows.zip" -OutFile "android-ndk.zip"
Expand-Archive -Path "android-ndk.zip" -DestinationPath "./android-ndk"
Get-ChildItem -Path "./android-ndk"
$ndkRoot = "${{ github.workspace }}\android-ndk\android-ndk-r27c"
echo "NDK_ROOT=$ndkRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8
$ndkBinPath = "$ndkRoot\toolchains\llvm\prebuilt\windows-x86_64\bin"
echo $ndkBinPath | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8
- name: Set up dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- run: dotnet publish src/N_m3u8DL-RE -r linux-bionic-x64 -p:DisableUnsupportedError=true -p:PublishAotUsingRuntimePack=true -o artifact
- run: dotnet publish src/N_m3u8DL-RE -r linux-bionic-arm64 -p:DisableUnsupportedError=true -p:PublishAotUsingRuntimePack=true -o artifact-arm64
- name: Package [linux-bionic]
run: |
cd artifact
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
cd ../artifact-arm64
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
- name: Upload Artifact [linux-bionic-x64]
uses: actions/upload-artifact@v3.1.3
with:
name: android-bionic-x64
path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-x64_${{ needs.set-date.outputs.date }}.tar.gz
- name: Upload Artifact[linux-bionic-arm64]
uses: actions/upload-artifact@v3.1.3
with:
name: android-bionic-arm64
path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_android-bionic-arm64_${{ needs.set-date.outputs.date }}.tar.gz
build-linux-musl-x64:
runs-on: ubuntu-latest
needs: set-date
container: mcr.microsoft.com/dotnet/sdk:9.0-alpine-amd64
steps:
- uses: actions/checkout@v1
- run: apk add clang build-base zlib-dev
- run: dotnet publish src/N_m3u8DL-RE -r linux-musl-x64 -c Release -o artifact -p:InvariantGlobalization=true
- name: Package [linux-musl-x64]
run: |
cd artifact
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-musl-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
- name: Upload Artifact [linux-musl-x64]
uses: actions/upload-artifact@v3.1.3
with:
name: linux-musl-x64
path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-musl-x64_${{ needs.set-date.outputs.date }}.tar.gz
build-linux-musl-arm64:
runs-on: ubuntu-latest
needs: set-date
container: mcr.microsoft.com/dotnet-buildtools/prereqs:ubuntu-22.04-cross-arm64-alpine
steps:
- uses: actions/checkout@v1
- name: Set up dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_SDK_VERSION }}
- run: apt-get update
- run: apt-get install -y build-essential clang binutils-aarch64-linux-gnu
- run: dotnet publish src/N_m3u8DL-RE -r linux-musl-arm64 -c Release -o artifact -p:CppCompilerAndLinker=clang -p:SysRoot=/crossrootfs/arm64 -p:InvariantGlobalization=true
- name: Package [linux-musl-arm64]
run: |
cd artifact
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-musl-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
- name: Upload Artifact [linux-musl-arm64]
uses: actions/upload-artifact@v3.1.3
with:
name: linux-musl-arm64
path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_linux-musl-arm64_${{ needs.set-date.outputs.date }}.tar.gz
build-mac-x64-arm64: build-mac-x64-arm64:
runs-on: macos-latest runs-on: macos-latest
needs: set-date
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
@ -101,83 +268,44 @@ jobs:
- run: dotnet publish src/N_m3u8DL-RE -r osx-arm64 -c Release -o artifact-arm64 - run: dotnet publish src/N_m3u8DL-RE -r osx-arm64 -c Release -o artifact-arm64
- run: dotnet publish src/N_m3u8DL-RE -r osx-x64 -c Release -o artifact-x64 - run: dotnet publish src/N_m3u8DL-RE -r osx-x64 -c Release -o artifact-x64
- name: Upload Artifact[osx-x64] - name: Package [osx]
run: |
cd artifact-x64
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-x64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
cd ../artifact-arm64
tar -czvf ../N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-arm64_${{ needs.set-date.outputs.date }}.tar.gz N_m3u8DL-RE
- name: Upload Artifact [osx-x64]
uses: actions/upload-artifact@v3.1.3 uses: actions/upload-artifact@v3.1.3
with: with:
name: N_m3u8DL-RE_Beta_osx-x64 name: osx-x64
path: artifact-x64/N_m3u8DL-RE path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-x64_${{ needs.set-date.outputs.date }}.tar.gz
- name: Upload Artifact[osx-arm64] - name: Upload Artifact[osx-arm64]
uses: actions/upload-artifact@v3.1.3 uses: actions/upload-artifact@v3.1.3
with: with:
name: N_m3u8DL-RE_Beta_osx-arm64 name: osx-arm64
path: artifact-arm64/N_m3u8DL-RE path: N_m3u8DL-RE_${{ needs.set-date.outputs.tag }}_osx-arm64_${{ needs.set-date.outputs.date }}.tar.gz
create_draft_release: create_release:
name: Create Github draft release name: Create release
if: ${{ github.event.inputs.doRelease == 'true' }}
needs: [build-win-x64-arm64,build-linux-x64,build-linux-arm64,build-mac-x64-arm64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: permissions:
- name: Audit gh version contents: write
run: gh --version
- name: Check for existing release
id: check_release
run: |
echo "::echo::on"
gh release view --repo '${{ github.repository }}' '${{ github.event.inputs.tag }}' \
&& echo "already_exists=true" >> $GITHUB_ENV \
|| echo "already_exists=false" >> $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repo
if: env.already_exists == 'false'
uses: actions/checkout@v3
with:
ref: '${{ github.event.inputs.ref }}'
- name: Create release
if: env.already_exists == 'false'
run: >
gh release create
'${{ github.event.inputs.tag }}'
--draft
--repo '${{ github.repository }}'
--title '${{ github.event.inputs.tag }}'
--target '${{ github.event.inputs.ref }}'
--generate-notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
attach_to_release:
name: Attach native executables to release
if: ${{ github.event.inputs.doRelease == 'true' }} if: ${{ github.event.inputs.doRelease == 'true' }}
needs: create_draft_release needs: [set-date,build-win-nt6_0-x86,build-win-x64-arm64,build-linux-x64-arm64,build-android-bionic-x64-arm64,build-linux-musl-x64,build-linux-musl-arm64,build-mac-x64-arm64]
runs-on: ubuntu-latest
steps: steps:
- name: Get current date - name: Fetch artifacts
id: date
run: echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
- name: GH version
run: gh --version
- name: Fetch executables
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
- name: Tar (linux, macOS) - name: Create GitHub Release
run: for dir in *{osx,linux}*; do tar cvzfp "${dir}_${{ env.date }}.tar.gz" "$dir"; done uses: ncipollo/release-action@v1
with:
- name: Zip (windows) tag: ${{ github.event.inputs.tag }}
run: for dir in *win*; do zip -r "${dir}_${{ env.date }}.zip" "$dir"; done name: N_m3u8DL-RE_${{ github.event.inputs.tag }}
artifacts: "android-bionic-x64/*,android-bionic-arm64/*,linux-x64/*,linux-arm64/*,linux-musl-x64/*,linux-musl-arm64/*,osx-x64/*,osx-arm64/*,win-x64/*,win-arm64/*,win-NT6.0-x86/*"
- name: Upload draft: false
run: | allowUpdates: true
until gh release upload --clobber --repo ${{ github.repository }} ${{ github.event.inputs.tag }} *.zip *.tar.gz; do generateReleaseNotes: true
echo "Attempt $((++attempts)) to upload release artifacts failed. Will retry in 20s" discussionCategory: 'Announcements'
sleep 20
done
timeout-minutes: 10
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -25,7 +25,7 @@ yay -Syu n-m3u8dl-re-git
# 命令行参数 # 命令行参数
``` ```
Description: Description:
N_m3u8DL-RE (Beta version) 20240630 N_m3u8DL-RE (Beta version) 20241201
Usage: Usage:
N_m3u8DL-RE <input> [options] N_m3u8DL-RE <input> [options]
@ -40,6 +40,7 @@ Options:
--base-url <base-url> 设置BaseURL --base-url <base-url> 设置BaseURL
--thread-count <number> 设置下载线程数 [default: 本机CPU线程数] --thread-count <number> 设置下载线程数 [default: 本机CPU线程数]
--download-retry-count <number> 每个分片下载异常时的重试次数 [default: 3] --download-retry-count <number> 每个分片下载异常时的重试次数 [default: 3]
--http-request-timeout <seconds> HTTP请求的超时时间(秒) [default: 100]
--force-ansi-console 强制认定终端为支持ANSI且可交互的终端 --force-ansi-console 强制认定终端为支持ANSI且可交互的终端
--no-ansi-color 去除ANSI颜色 --no-ansi-color 去除ANSI颜色
--auto-select 自动选择所有类型的最佳轨道 [default: False] --auto-select 自动选择所有类型的最佳轨道 [default: False]
@ -63,11 +64,12 @@ Options:
--log-level <DEBUG|ERROR|INFO|OFF|WARN> 设置日志级别 [default: INFO] --log-level <DEBUG|ERROR|INFO|OFF|WARN> 设置日志级别 [default: INFO]
--ui-language <en-US|zh-CN|zh-TW> 设置UI语言 --ui-language <en-US|zh-CN|zh-TW> 设置UI语言
--urlprocessor-args <urlprocessor-args> 此字符串将直接传递给URL Processor --urlprocessor-args <urlprocessor-args> 此字符串将直接传递给URL Processor
--key <key> 设置解密密钥, 程序调用mp4decrpyt/shaka-packager进行解密. 格式: --key <key> 设置解密密钥, 程序调用mp4decrpyt/shaka-packager/ffmpeg进行解密. 格式:
--key KID1:KEY1 --key KID2:KEY2 --key KID1:KEY1 --key KID2:KEY2
对于KEY相同的情况可以直接输入 --key KEY
--key-text-file <key-text-file> 设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件) --key-text-file <key-text-file> 设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)
--decryption-engine <FFMPEG|MP4DECRYPT|SHAKA_PACKAGER> 设置解密时使用的第三方程序 [default: MP4DECRYPT]
--decryption-binary-path <PATH> MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe --decryption-binary-path <PATH> MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe
--use-shaka-packager 解密时使用shaka-packager替代mp4decrypt [default: False]
--mp4-real-time-decryption 实时解密MP4分片 [default: False] --mp4-real-time-decryption 实时解密MP4分片 [default: False]
-R, --max-speed <SPEED> 设置限速,单位支持 Mbps 或 Kbps15M 100K -R, --max-speed <SPEED> 设置限速,单位支持 Mbps 或 Kbps15M 100K
-M, --mux-after-done <OPTIONS> 所有工作完成时尝试混流分离的音视频. 输入 "--morehelp mux-after-done" 以查看详细信息 -M, --mux-after-done <OPTIONS> 所有工作完成时尝试混流分离的音视频. 输入 "--morehelp mux-after-done" 以查看详细信息
@ -94,6 +96,8 @@ Options:
-da, --drop-audio <OPTIONS> 通过正则表达式去除符合要求的音频流. -da, --drop-audio <OPTIONS> 通过正则表达式去除符合要求的音频流.
-ds, --drop-subtitle <OPTIONS> 通过正则表达式去除符合要求的字幕流. -ds, --drop-subtitle <OPTIONS> 通过正则表达式去除符合要求的字幕流.
--ad-keyword <REG> 设置广告分片的URL关键字(正则表达式) --ad-keyword <REG> 设置广告分片的URL关键字(正则表达式)
--disable-update-check 禁用版本更新检测 [default: False]
--allow-hls-multi-ext-map 允许HLS中的多个#EXT-X-MAP(实验性) [default: False]
--morehelp <OPTION> 查看某个选项的详细帮助信息 --morehelp <OPTION> 查看某个选项的详细帮助信息
--version Show version information --version Show version information
-?, -h, --help Show help and usage information -?, -h, --help Show help and usage information

View File

@ -1,14 +1,9 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity namespace N_m3u8DL_RE.Common.Entity;
public class EncryptInfo
{ {
public class EncryptInfo
{
/// <summary> /// <summary>
/// 加密方式,默认无加密 /// 加密方式,默认无加密
/// </summary> /// </summary>
@ -34,10 +29,6 @@ namespace N_m3u8DL_RE.Common.Entity
{ {
return m; return m;
} }
else
{
return EncryptMethod.UNKNOWN; return EncryptMethod.UNKNOWN;
} }
}
}
} }

View File

@ -1,13 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity public class MSSData
{ {
public class MSSData
{
public string FourCC { get; set; } = ""; public string FourCC { get; set; } = "";
public string CodecPrivateData { get; set; } = ""; public string CodecPrivateData { get; set; } = "";
public string Type { get; set; } = ""; public string Type { get; set; } = "";
@ -21,5 +15,4 @@ namespace N_m3u8DL_RE.Common.Entity
public bool IsProtection { get; set; } = false; public bool IsProtection { get; set; } = false;
public string ProtectionSystemID { get; set; } = ""; public string ProtectionSystemID { get; set; } = "";
public string ProtectionData { get; set; } = ""; public string ProtectionData { get; set; } = "";
}
} }

View File

@ -1,14 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity // 主要处理 EXT-X-DISCONTINUITY
public class MediaPart
{ {
//主要处理 EXT-X-DISCONTINUITY public List<MediaSegment> MediaSegments { get; set; } = [];
public class MediaPart
{
public List<MediaSegment> MediaSegments { get; set; } = new List<MediaSegment>();
}
} }

View File

@ -1,33 +1,31 @@
using System; using N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity namespace N_m3u8DL_RE.Common.Entity;
public class MediaSegment
{ {
public class MediaSegment
{
public long Index { get; set; } public long Index { get; set; }
public double Duration { get; set; } public double Duration { get; set; }
public string? Title { get; set; } public string? Title { get; set; }
public DateTime? DateTime { get; set; } public DateTime? DateTime { get; set; }
public long? StartRange { get; set; } public long? StartRange { get; set; }
public long? StopRange { get => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null; } public long? StopRange => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null;
public long? ExpectLength { get; set; } public long? ExpectLength { get; set; }
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo(); public EncryptInfo EncryptInfo { get; set; } = new();
public string Url { get; set; } public bool IsEncrypted => EncryptInfo.Method != EncryptMethod.NONE;
public string? NameFromVar { get; set; } //MPD分段文件名 public string Url { get; set; } = string.Empty;
public string? NameFromVar { get; set; } // MPD分段文件名
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
return obj is MediaSegment segment && return obj is MediaSegment segment &&
Index == segment.Index && Index == segment.Index &&
Duration == segment.Duration && Math.Abs(Duration - segment.Duration) < 0.001 &&
Title == segment.Title && Title == segment.Title &&
StartRange == segment.StartRange && StartRange == segment.StartRange &&
StopRange == segment.StopRange && StopRange == segment.StopRange &&
@ -39,5 +37,4 @@ namespace N_m3u8DL_RE.Common.Entity
{ {
return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url); return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url);
} }
}
} }

View File

@ -1,27 +1,20 @@
using N_m3u8DL_RE.Common.Enum; namespace N_m3u8DL_RE.Common.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity public class Playlist
{ {
public class Playlist // 对应Url信息
{ public string Url { get; set; } = string.Empty;
//对应Url信息 // 是否直播
public string Url { get; set; }
//是否直播
public bool IsLive { get; set; } = false; public bool IsLive { get; set; } = false;
//直播刷新间隔毫秒默认15秒 // 直播刷新间隔毫秒默认15秒
public double RefreshIntervalMs { get; set; } = 15000; public double RefreshIntervalMs { get; set; } = 15000;
//所有分片时长总和 // 所有分片时长总和
public double TotalDuration { get => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration)); } public double TotalDuration => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration));
//所有分片中最长时长
// 所有分片中最长时长
public double? TargetDuration { get; set; } public double? TargetDuration { get; set; }
//INIT信息 // INIT信息
public MediaSegment? MediaInit { get; set; } public MediaSegment? MediaInit { get; set; }
//分片信息 // 分片信息
public List<MediaPart> MediaParts { get; set; } = new List<MediaPart>(); public List<MediaPart> MediaParts { get; set; } = [];
}
} }

View File

@ -1,29 +1,24 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity namespace N_m3u8DL_RE.Common.Entity;
public class StreamSpec
{ {
public class StreamSpec
{
public MediaType? MediaType { get; set; } public MediaType? MediaType { get; set; }
public string? GroupId { get; set; } public string? GroupId { get; set; }
public string? Language { get; set; } public string? Language { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public Choise? Default { get; set; } public Choise? Default { get; set; }
//由于用户选择 被跳过的分片总时长 // 由于用户选择 被跳过的分片总时长
public double? SkippedDuration { get; set; } public double? SkippedDuration { get; set; }
//MSS信息 // MSS信息
public MSSData? MSSData { get; set; } public MSSData? MSSData { get; set; }
//基本信息 // 基本信息
public int? Bandwidth { get; set; } public int? Bandwidth { get; set; }
public string? Codecs { get; set; } public string? Codecs { get; set; }
public string? Resolution { get; set; } public string? Resolution { get; set; }
@ -31,17 +26,17 @@ namespace N_m3u8DL_RE.Common.Entity
public string? Channels { get; set; } public string? Channels { get; set; }
public string? Extension { get; set; } public string? Extension { get; set; }
//Dash // Dash
public RoleType? Role { get; set; } public RoleType? Role { get; set; }
//补充信息-色域 // 补充信息-色域
public string? VideoRange { get; set; } public string? VideoRange { get; set; }
//补充信息-特征 // 补充信息-特征
public string? Characteristics { get; set; } public string? Characteristics { get; set; }
//发布时间仅MPD需要 // 发布时间仅MPD需要
public DateTime? PublishTime { get; set; } public DateTime? PublishTime { get; set; }
//外部轨道GroupId (后续寻找对应轨道信息) // 外部轨道GroupId (后续寻找对应轨道信息)
public string? AudioId { get; set; } public string? AudioId { get; set; }
public string? VideoId { get; set; } public string? VideoId { get; set; }
public string? SubtitleId { get; set; } public string? SubtitleId { get; set; }
@ -51,12 +46,12 @@ namespace N_m3u8DL_RE.Common.Entity
/// <summary> /// <summary>
/// URL /// URL
/// </summary> /// </summary>
public string Url { get; set; } public string Url { get; set; } = string.Empty;
/// <summary> /// <summary>
/// 原始URL /// 原始URL
/// </summary> /// </summary>
public string OriginalUrl { get; set; } public string OriginalUrl { get; set; } = string.Empty;
public Playlist? Playlist { get; set; } public Playlist? Playlist { get; set; }
@ -143,7 +138,7 @@ namespace N_m3u8DL_RE.Common.Entity
var encStr = string.Empty; var encStr = string.Empty;
var segmentsCountStr = SegmentsCount == 0 ? "" : (SegmentsCount > 1 ? $"{SegmentsCount} Segments" : $"{SegmentsCount} Segment"); var segmentsCountStr = SegmentsCount == 0 ? "" : (SegmentsCount > 1 ? $"{SegmentsCount} Segments" : $"{SegmentsCount} Segment");
//增加加密标志 // 增加加密标志
if (Playlist != null && Playlist.MediaParts.Any(m => m.MediaSegments.Any(s => s.EncryptInfo.Method != EncryptMethod.NONE))) if (Playlist != null && Playlist.MediaParts.Any(m => m.MediaSegments.Any(s => s.EncryptInfo.Method != EncryptMethod.NONE)))
{ {
var ms = Playlist.MediaParts.SelectMany(m => m.MediaSegments.Select(s => s.EncryptInfo.Method)).Where(e => e != EncryptMethod.NONE).Distinct(); var ms = Playlist.MediaParts.SelectMany(m => m.MediaSegments.Select(s => s.EncryptInfo.Method)).Where(e => e != EncryptMethod.NONE).Distinct();
@ -175,7 +170,7 @@ namespace N_m3u8DL_RE.Common.Entity
returnStr = returnStr.Replace("| |", "|"); returnStr = returnStr.Replace("| |", "|");
} }
//计算时长 // 计算时长
if (Playlist != null) if (Playlist != null)
{ {
var total = Playlist.TotalDuration; var total = Playlist.TotalDuration;
@ -184,5 +179,4 @@ namespace N_m3u8DL_RE.Common.Entity
return returnStr.TrimEnd().TrimEnd('|').TrimEnd(); return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
} }
}
} }

View File

@ -1,13 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity public class SubCue
{ {
public class SubCue
{
public TimeSpan StartTime { get; set; } public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; } public TimeSpan EndTime { get; set; }
public required string Payload { get; set; } public required string Payload { get; set; }
@ -26,5 +20,4 @@ namespace N_m3u8DL_RE.Common.Entity
{ {
return HashCode.Combine(StartTime, EndTime, Payload, Settings); return HashCode.Combine(StartTime, EndTime, Payload, Settings);
} }
}
} }

View File

@ -1,24 +1,20 @@
using System; using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Entity namespace N_m3u8DL_RE.Common.Entity;
public partial class WebVttSub
{ {
public partial class WebVttSub
{
[GeneratedRegex("X-TIMESTAMP-MAP.*")] [GeneratedRegex("X-TIMESTAMP-MAP.*")]
private static partial Regex TSMapRegex(); private static partial Regex TSMapRegex();
[GeneratedRegex("MPEGTS:(\\d+)")] [GeneratedRegex("MPEGTS:(\\d+)")]
private static partial Regex TSValueRegex(); private static partial Regex TSValueRegex();
[GeneratedRegex("\\s")] [GeneratedRegex("\\s")]
private static partial Regex SplitRegex(); private static partial Regex SplitRegex();
[GeneratedRegex("<c\\..*?>([\\s\\S]*?)<\\/c>")] [GeneratedRegex(@"<c\..*?>([\s\S]*?)<\/c>")]
private static partial Regex VttClassRegex(); private static partial Regex VttClassRegex();
public List<SubCue> Cues { get; set; } = new List<SubCue>(); public List<SubCue> Cues { get; set; } = [];
public long MpegtsTimestamp { get; set; } = 0L; public long MpegtsTimestamp { get; set; } = 0L;
/// <summary> /// <summary>
@ -75,12 +71,12 @@ namespace N_m3u8DL_RE.Common.Entity
continue; continue;
} }
if (needPayload) if (!needPayload) continue;
{
if (string.IsNullOrEmpty(line.Trim())) if (string.IsNullOrEmpty(line.Trim()))
{ {
var payload = string.Join(Environment.NewLine, payloads); var payload = string.Join(Environment.NewLine, payloads);
if (string.IsNullOrEmpty(payload.Trim())) continue; //没获取到payload 跳过添加 if (string.IsNullOrEmpty(payload.Trim())) continue; // 没获取到payload 跳过添加
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList(); var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
var startTime = ConvertToTS(arr[0]); var startTime = ConvertToTS(arr[0]);
@ -90,7 +86,7 @@ namespace N_m3u8DL_RE.Common.Entity
{ {
StartTime = startTime, StartTime = startTime,
EndTime = endTime, EndTime = endTime,
Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), //Remove Zero Width Space! Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), // Remove Zero Width Space!
Settings = style Settings = style
}); });
payloads.Clear(); payloads.Clear();
@ -101,10 +97,9 @@ namespace N_m3u8DL_RE.Common.Entity
payloads.Add(line.Trim()); payloads.Add(line.Trim());
} }
} }
}
if (BaseTimestamp != 0) if (BaseTimestamp == 0) return webSub;
{
foreach (var item in webSub.Cues) foreach (var item in webSub.Cues)
{ {
if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0) if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)
@ -117,7 +112,6 @@ namespace N_m3u8DL_RE.Common.Entity
break; break;
} }
} }
}
return webSub; return webSub;
} }
@ -131,7 +125,7 @@ namespace N_m3u8DL_RE.Common.Entity
return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " ")); return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " "));
})).TrimEnd(); })).TrimEnd();
} }
else return text; return text;
} }
/// <summary> /// <summary>
@ -144,9 +138,9 @@ namespace N_m3u8DL_RE.Common.Entity
FixTimestamp(webSub, this.MpegtsTimestamp); FixTimestamp(webSub, this.MpegtsTimestamp);
foreach (var item in webSub.Cues) foreach (var item in webSub.Cues)
{ {
if (!this.Cues.Contains(item)) if (this.Cues.Contains(item)) continue;
{
//如果相差只有1ms且payload相同则拼接 // 如果相差只有1ms且payload相同则拼接
var last = this.Cues.LastOrDefault(); var last = this.Cues.LastOrDefault();
if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload) if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload)
{ {
@ -157,7 +151,6 @@ namespace N_m3u8DL_RE.Common.Entity
this.Cues.Add(item); this.Cues.Add(item);
} }
} }
}
return this; return this;
} }
@ -168,19 +161,19 @@ namespace N_m3u8DL_RE.Common.Entity
return; return;
} }
//确实存在时间轴错误的情况,才修复 // 确实存在时间轴错误的情况,才修复
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) 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)
{ {
//The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second // The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second
var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000; var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;
var offset = TimeSpan.FromSeconds(seconds); var offset = TimeSpan.FromSeconds(seconds);
//当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒而字幕起始却是2秒),才修复 // 当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒而字幕起始却是2秒),才修复
if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset) if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset)
{ {
for (int i = 0; i < sub.Cues.Count; i++) foreach (var subCue in sub.Cues)
{ {
sub.Cues[i].StartTime += offset; subCue.StartTime += offset;
sub.Cues[i].EndTime += offset; subCue.EndTime += offset;
} }
} }
} }
@ -193,7 +186,7 @@ namespace N_m3u8DL_RE.Common.Entity
private static TimeSpan ConvertToTS(string str) private static TimeSpan ConvertToTS(string str)
{ {
//17.0s // 17.0s
if (str.EndsWith('s')) if (str.EndsWith('s'))
{ {
double sec = Convert.ToDouble(str[..^1]); double sec = Convert.ToDouble(str[..^1]);
@ -201,11 +194,15 @@ namespace N_m3u8DL_RE.Common.Entity
} }
str = str.Replace(',', '.'); str = str.Replace(',', '.');
var ms = Convert.ToInt32(str.Split('.').Last()); long time = 0;
var o = str.Split('.').First(); string[] parts = str.Split('.');
var t = o.Split(':').Reverse().ToList(); if (parts.Length > 1)
var time = 0L + ms; {
for (int i = 0; i < t.Count(); i++) time += Convert.ToInt32(parts.Last().PadRight(3, '0'));
str = parts.First();
}
var t = str.Split(':').Reverse().ToList();
for (int i = 0; i < t.Count; i++)
{ {
time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000; time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
} }
@ -214,8 +211,8 @@ namespace N_m3u8DL_RE.Common.Entity
public override string ToString() public override string ToString()
{ {
StringBuilder sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var c in GetCues()) //输出时去除空串 foreach (var c in GetCues()) // 输出时去除空串
{ {
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings); sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\.fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\.fff") + " " + c.Settings);
sb.AppendLine(c.Payload); sb.AppendLine(c.Payload);
@ -263,10 +260,9 @@ namespace N_m3u8DL_RE.Common.Entity
if (string.IsNullOrEmpty(srt.Trim())) if (string.IsNullOrEmpty(srt.Trim()))
{ {
srt = "1\r\n00:00:00,000 --> 00:00:01,000"; //空字幕 srt = "1\r\n00:00:00,000 --> 00:00:01,000"; // 空字幕
} }
return srt; return srt;
} }
}
} }

View File

@ -1,14 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum Choise
{ {
public enum Choise
{
YES = 1, YES = 1,
NO = 0 NO = 0
}
} }

View File

@ -1,13 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum EncryptMethod
{ {
public enum EncryptMethod
{
NONE, NONE,
AES_128, AES_128,
AES_128_ECB, AES_128_ECB,
@ -16,5 +10,4 @@ namespace N_m3u8DL_RE.Common.Enum
CENC, CENC,
CHACHA20, CHACHA20,
UNKNOWN UNKNOWN
}
} }

View File

@ -1,16 +1,9 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum ExtractorType
{ {
public enum ExtractorType
{
MPEG_DASH, MPEG_DASH,
HLS, HLS,
HTTP_LIVE, HTTP_LIVE,
MSS MSS
}
} }

View File

@ -1,16 +1,9 @@
using System; namespace N_m3u8DL_RE.Common.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Enum public enum MediaType
{ {
public enum MediaType
{
AUDIO = 0, AUDIO = 0,
VIDEO = 1, VIDEO = 1,
SUBTITLES = 2, SUBTITLES = 2,
CLOSED_CAPTIONS = 3 CLOSED_CAPTIONS = 3
}
} }

View File

@ -1,7 +1,7 @@
namespace N_m3u8DL_RE.Common.Enum namespace N_m3u8DL_RE.Common.Enum;
public enum RoleType
{ {
public enum RoleType
{
Subtitle = 0, Subtitle = 0,
Main = 1, Main = 1,
Alternate = 2, Alternate = 2,
@ -11,5 +11,5 @@
Description = 6, Description = 6,
Sign = 7, Sign = 7,
Metadata = 8, Metadata = 8,
} ForcedSubtitle = 9
} }

View File

@ -2,21 +2,20 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace N_m3u8DL_RE.Common namespace N_m3u8DL_RE.Common;
{
[JsonSourceGenerationOptions( [JsonSourceGenerationOptions(
WriteIndented = true, WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
GenerationMode = JsonSourceGenerationMode.Metadata)] GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(MediaType))] [JsonSerializable(typeof(MediaType))]
[JsonSerializable(typeof(EncryptMethod))] [JsonSerializable(typeof(EncryptMethod))]
[JsonSerializable(typeof(ExtractorType))] [JsonSerializable(typeof(ExtractorType))]
[JsonSerializable(typeof(Choise))] [JsonSerializable(typeof(Choise))]
[JsonSerializable(typeof(StreamSpec))] [JsonSerializable(typeof(StreamSpec))]
[JsonSerializable(typeof(IOrderedEnumerable<StreamSpec>))] [JsonSerializable(typeof(IOrderedEnumerable<StreamSpec>))]
[JsonSerializable(typeof(IEnumerable<MediaSegment>))] [JsonSerializable(typeof(IEnumerable<MediaSegment>))]
[JsonSerializable(typeof(List<StreamSpec>))] [JsonSerializable(typeof(List<StreamSpec>))]
[JsonSerializable(typeof(List<MediaSegment>))] [JsonSerializable(typeof(List<MediaSegment>))]
[JsonSerializable(typeof(Dictionary<string, string>))] [JsonSerializable(typeof(Dictionary<string, string>))]
internal partial class JsonContext : JsonSerializerContext { } internal partial class JsonContext : JsonSerializerContext { }
}

View File

@ -1,17 +1,11 @@
using System; using System.Text.Json;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.JsonConverter namespace N_m3u8DL_RE.Common.JsonConverter;
internal class BytesBase64Converter : JsonConverter<byte[]>
{ {
internal class BytesBase64Converter : JsonConverter<byte[]>
{
public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetBytesFromBase64(); public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.GetBytesFromBase64();
public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) => writer.WriteStringValue(Convert.ToBase64String(value)); public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) => writer.WriteStringValue(Convert.ToBase64String(value));
}
} }

View File

@ -4,39 +4,46 @@ using Spectre.Console;
namespace N_m3u8DL_RE.Common.Log; namespace N_m3u8DL_RE.Common.Log;
public class NonAnsiWriter : TextWriter public partial class NonAnsiWriter : TextWriter
{ {
public override Encoding Encoding => Console.OutputEncoding; public override Encoding Encoding => Console.OutputEncoding;
private string lastOut = ""; private string? _lastOut = "";
public override void Write(char value) public override void Write(char value)
{ {
Console.Write(value); Console.Write(value);
} }
public override void Write(string value) public override void Write(string? value)
{ {
if (lastOut == value) if (_lastOut == value)
{ {
return; return;
} }
lastOut = value; _lastOut = value;
RemoveAnsiEscapeSequences(value); RemoveAnsiEscapeSequences(value);
} }
private void RemoveAnsiEscapeSequences(string input) private void RemoveAnsiEscapeSequences(string? input)
{ {
// Use regular expression to remove ANSI escape sequences // Use regular expression to remove ANSI escape sequences
string output = Regex.Replace(input, @"\x1B\[(\d+;?)+m", ""); var output = MyRegex().Replace(input ?? "", "");
output = Regex.Replace(output, @"\[\??\d+[AKlh]", ""); output = MyRegex1().Replace(output, "");
output = Regex.Replace(output,"[\r\n] +",""); output = MyRegex2().Replace(output, "");
if (string.IsNullOrWhiteSpace(output)) if (string.IsNullOrWhiteSpace(output))
{ {
return; return;
} }
Console.Write(output); Console.Write(output);
} }
[GeneratedRegex(@"\x1B\[(\d+;?)+m")]
private static partial Regex MyRegex();
[GeneratedRegex(@"\[\??\d+[AKlh]")]
private static partial Regex MyRegex1();
[GeneratedRegex("[\r\n] +")]
private static partial Regex MyRegex2();
} }
/// <summary> /// <summary>

View File

@ -1,17 +1,10 @@
using System; namespace N_m3u8DL_RE.Common.Log;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Log public enum LogLevel
{ {
public enum LogLevel
{
OFF, OFF,
ERROR, ERROR,
WARN, WARN,
INFO, INFO,
DEBUG, DEBUG,
}
} }

View File

@ -1,17 +1,11 @@
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace N_m3u8DL_RE.Common.Log namespace N_m3u8DL_RE.Common.Log;
public static partial class Logger
{ {
public partial class Logger
{
[GeneratedRegex("{}")] [GeneratedRegex("{}")]
private static partial Regex VarsRepRegex(); private static partial Regex VarsRepRegex();
@ -30,7 +24,7 @@ namespace N_m3u8DL_RE.Common.Log
/// </summary> /// </summary>
private static string? LogFilePath { get; set; } private static string? LogFilePath { get; set; }
//读写锁 // 读写锁
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
public static void InitLogFile() public static void InitLogFile()
@ -54,7 +48,7 @@ namespace N_m3u8DL_RE.Common.Log
+ "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine + "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine
+ "Task CommandLine: " + Environment.CommandLine; + "Task CommandLine: " + Environment.CommandLine;
init += $"{Environment.NewLine}{Environment.NewLine}"; init += $"{Environment.NewLine}{Environment.NewLine}";
//若文件存在则加序号 // 若文件存在则加序号
while (File.Exists(LogFilePath)) while (File.Exists(LogFilePath))
{ {
LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log"); LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log");
@ -86,12 +80,12 @@ namespace N_m3u8DL_RE.Common.Log
Console.WriteLine(subWrite); Console.WriteLine(subWrite);
} }
if (IsWriteFile && File.Exists(LogFilePath)) if (!IsWriteFile || !File.Exists(LogFilePath)) return;
{
var plain = write.RemoveMarkup() + subWrite.RemoveMarkup(); var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();
try try
{ {
//进入写入 // 进入写入
LogWriteLock.EnterWriteLock(); LogWriteLock.EnterWriteLock();
using (StreamWriter sw = File.AppendText(LogFilePath)) using (StreamWriter sw = File.AppendText(LogFilePath))
{ {
@ -100,11 +94,10 @@ namespace N_m3u8DL_RE.Common.Log
} }
finally finally
{ {
//释放占用 // 释放占用
LogWriteLock.ExitWriteLock(); LogWriteLock.ExitWriteLock();
} }
} }
}
catch (Exception) catch (Exception)
{ {
Console.WriteLine("Failed to write: " + write); Console.WriteLine("Failed to write: " + write);
@ -123,83 +116,75 @@ namespace N_m3u8DL_RE.Common.Log
public static void Info(string data, params object[] ps) public static void Info(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.INFO) if (LogLevel < LogLevel.INFO) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : "; var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
HandleLog(write, data); HandleLog(write, data);
} }
}
public static void InfoMarkUp(string data, params object[] ps) public static void InfoMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.INFO) if (LogLevel < LogLevel.INFO) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data; var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
HandleLog(write); HandleLog(write);
} }
}
public static void Debug(string data, params object[] ps) public static void Debug(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.DEBUG) if (LogLevel < LogLevel.DEBUG) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: "; var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
HandleLog(write, data); HandleLog(write, data);
} }
}
public static void DebugMarkUp(string data, params object[] ps) public static void DebugMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.DEBUG) if (LogLevel < LogLevel.DEBUG) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data; var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
HandleLog(write); HandleLog(write);
} }
}
public static void Warn(string data, params object[] ps) public static void Warn(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.WARN) if (LogLevel < LogLevel.WARN) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : "; var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
HandleLog(write, data); HandleLog(write, data);
} }
}
public static void WarnMarkUp(string data, params object[] ps) public static void WarnMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.WARN) if (LogLevel < LogLevel.WARN) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data; var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
HandleLog(write); HandleLog(write);
} }
}
public static void Error(string data, params object[] ps) public static void Error(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.ERROR) if (LogLevel < LogLevel.ERROR) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: "; var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
HandleLog(write, data); HandleLog(write, data);
} }
}
public static void ErrorMarkUp(string data, params object[] ps) public static void ErrorMarkUp(string data, params object[] ps)
{ {
if (LogLevel >= LogLevel.ERROR) if (LogLevel < LogLevel.ERROR) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data; var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
HandleLog(write); HandleLog(write);
} }
}
public static void ErrorMarkUp(Exception exception) public static void ErrorMarkUp(Exception exception)
{ {
@ -219,13 +204,13 @@ namespace N_m3u8DL_RE.Common.Log
/// <param name="ps"></param> /// <param name="ps"></param>
public static void Extra(string data, params object[] ps) public static void Extra(string data, params object[] ps)
{ {
if (IsWriteFile && File.Exists(LogFilePath)) if (!IsWriteFile || !File.Exists(LogFilePath)) return;
{
data = ReplaceVars(data, ps); data = ReplaceVars(data, ps);
var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup(); var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup();
try try
{ {
//进入写入 // 进入写入
LogWriteLock.EnterWriteLock(); LogWriteLock.EnterWriteLock();
using (StreamWriter sw = File.AppendText(LogFilePath)) using (StreamWriter sw = File.AppendText(LogFilePath))
{ {
@ -234,10 +219,8 @@ namespace N_m3u8DL_RE.Common.Log
} }
finally finally
{ {
//释放占用 // 释放占用
LogWriteLock.ExitWriteLock(); LogWriteLock.ExitWriteLock();
} }
} }
}
}
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>library</OutputType> <OutputType>library</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RootNamespace>N_m3u8DL_RE.Common</RootNamespace> <RootNamespace>N_m3u8DL_RE.Common</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.47.1-preview.0.11" /> <PackageReference Include="Spectre.Console" Version="0.49.2-preview.0.50" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,151 +1,150 @@
using System; namespace N_m3u8DL_RE.Common.Resource;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Resource public static class ResString
{ {
public class ResString public static string CurrentLoc { get; set; } = "en-US";
{
public readonly static string ReLiveTs = "<RE_LIVE_TS>"; public static readonly string ReLiveTs = "<RE_LIVE_TS>";
public static string singleFileRealtimeDecryptWarn { get => GetText("singleFileRealtimeDecryptWarn"); } public static string singleFileRealtimeDecryptWarn => GetText("singleFileRealtimeDecryptWarn");
public static string singleFileSplitWarn { get => GetText("singleFileSplitWarn"); } public static string singleFileSplitWarn => GetText("singleFileSplitWarn");
public static string customRangeWarn { get => GetText("customRangeWarn"); } public static string customRangeWarn => GetText("customRangeWarn");
public static string customRangeFound { get => GetText("customRangeFound"); } public static string customRangeFound => GetText("customRangeFound");
public static string customAdKeywordsFound { get => GetText("customAdKeywordsFound"); } public static string customAdKeywordsFound => GetText("customAdKeywordsFound");
public static string customRangeInvalid { get => GetText("customRangeInvalid"); } public static string customRangeInvalid => GetText("customRangeInvalid");
public static string consoleRedirected { get => GetText("consoleRedirected"); } public static string consoleRedirected => GetText("consoleRedirected");
public static string autoBinaryMerge { get => GetText("autoBinaryMerge"); } public static string autoBinaryMerge => GetText("autoBinaryMerge");
public static string autoBinaryMerge2 { get => GetText("autoBinaryMerge2"); } public static string autoBinaryMerge2 => GetText("autoBinaryMerge2");
public static string autoBinaryMerge3 { get => GetText("autoBinaryMerge3"); } public static string autoBinaryMerge3 => GetText("autoBinaryMerge3");
public static string autoBinaryMerge4 { get => GetText("autoBinaryMerge4"); } public static string autoBinaryMerge4 => GetText("autoBinaryMerge4");
public static string autoBinaryMerge5 { get => GetText("autoBinaryMerge5"); } public static string autoBinaryMerge5 => GetText("autoBinaryMerge5");
public static string autoBinaryMerge6 { get => GetText("autoBinaryMerge6"); } public static string autoBinaryMerge6 => GetText("autoBinaryMerge6");
public static string badM3u8 { get => GetText("badM3u8"); } public static string badM3u8 => GetText("badM3u8");
public static string binaryMerge { get => GetText("binaryMerge"); } public static string binaryMerge => GetText("binaryMerge");
public static string checkingLast { get => GetText("checkingLast"); } public static string checkingLast => GetText("checkingLast");
public static string cmd_appendUrlParams { get => GetText("cmd_appendUrlParams"); } public static string cmd_appendUrlParams => GetText("cmd_appendUrlParams");
public static string cmd_autoSelect { get => GetText("cmd_autoSelect"); } public static string cmd_autoSelect => GetText("cmd_autoSelect");
public static string cmd_binaryMerge { get => GetText("cmd_binaryMerge"); } public static string cmd_disableUpdateCheck => GetText("cmd_disableUpdateCheck");
public static string cmd_useFFmpegConcatDemuxer { get => GetText("cmd_useFFmpegConcatDemuxer"); } public static string cmd_binaryMerge => GetText("cmd_binaryMerge");
public static string cmd_checkSegmentsCount { get => GetText("cmd_checkSegmentsCount"); } public static string cmd_useFFmpegConcatDemuxer => GetText("cmd_useFFmpegConcatDemuxer");
public static string cmd_decryptionBinaryPath { get => GetText("cmd_decryptionBinaryPath"); } public static string cmd_checkSegmentsCount => GetText("cmd_checkSegmentsCount");
public static string cmd_delAfterDone { get => GetText("cmd_delAfterDone"); } public static string cmd_decryptionBinaryPath => GetText("cmd_decryptionBinaryPath");
public static string cmd_ffmpegBinaryPath { get => GetText("cmd_ffmpegBinaryPath"); } public static string cmd_delAfterDone => GetText("cmd_delAfterDone");
public static string cmd_mkvmergeBinaryPath { get => GetText("cmd_mkvmergeBinaryPath"); } public static string cmd_ffmpegBinaryPath => GetText("cmd_ffmpegBinaryPath");
public static string cmd_baseUrl { get => GetText("cmd_baseUrl"); } public static string cmd_mkvmergeBinaryPath => GetText("cmd_mkvmergeBinaryPath");
public static string cmd_maxSpeed { get => GetText("cmd_maxSpeed"); } public static string cmd_baseUrl => GetText("cmd_baseUrl");
public static string cmd_adKeyword { get => GetText("cmd_adKeyword"); } public static string cmd_maxSpeed => GetText("cmd_maxSpeed");
public static string cmd_moreHelp { get => GetText("cmd_moreHelp"); } public static string cmd_adKeyword => GetText("cmd_adKeyword");
public static string cmd_header { get => GetText("cmd_header"); } public static string cmd_moreHelp => GetText("cmd_moreHelp");
public static string cmd_muxImport { get => GetText("cmd_muxImport"); } public static string cmd_header => GetText("cmd_header");
public static string cmd_muxImport_more { get => GetText("cmd_muxImport_more"); } public static string cmd_muxImport => GetText("cmd_muxImport");
public static string cmd_selectVideo { get => GetText("cmd_selectVideo"); } public static string cmd_muxImport_more => GetText("cmd_muxImport_more");
public static string cmd_dropVideo { get => GetText("cmd_dropVideo"); } public static string cmd_selectVideo => GetText("cmd_selectVideo");
public static string cmd_selectVideo_more { get => GetText("cmd_selectVideo_more"); } public static string cmd_dropVideo => GetText("cmd_dropVideo");
public static string cmd_selectAudio { get => GetText("cmd_selectAudio"); } public static string cmd_selectVideo_more => GetText("cmd_selectVideo_more");
public static string cmd_dropAudio { get => GetText("cmd_dropAudio"); } public static string cmd_selectAudio => GetText("cmd_selectAudio");
public static string cmd_selectAudio_more { get => GetText("cmd_selectAudio_more"); } public static string cmd_dropAudio => GetText("cmd_dropAudio");
public static string cmd_selectSubtitle { get => GetText("cmd_selectSubtitle"); } public static string cmd_selectAudio_more => GetText("cmd_selectAudio_more");
public static string cmd_dropSubtitle { get => GetText("cmd_dropSubtitle"); } public static string cmd_selectSubtitle => GetText("cmd_selectSubtitle");
public static string cmd_selectSubtitle_more { get => GetText("cmd_selectSubtitle_more"); } public static string cmd_dropSubtitle => GetText("cmd_dropSubtitle");
public static string cmd_custom_range { get => GetText("cmd_custom_range"); } public static string cmd_selectSubtitle_more => GetText("cmd_selectSubtitle_more");
public static string cmd_customHLSMethod { get => GetText("cmd_customHLSMethod"); } public static string cmd_custom_range => GetText("cmd_custom_range");
public static string cmd_customHLSKey { get => GetText("cmd_customHLSKey"); } public static string cmd_customHLSMethod => GetText("cmd_customHLSMethod");
public static string cmd_customHLSIv { get => GetText("cmd_customHLSIv"); } public static string cmd_customHLSKey => GetText("cmd_customHLSKey");
public static string cmd_Input { get => GetText("cmd_Input"); } public static string cmd_customHLSIv => GetText("cmd_customHLSIv");
public static string cmd_forceAnsiConsole { get => GetText("cmd_forceAnsiConsole"); } public static string cmd_Input => GetText("cmd_Input");
public static string cmd_noAnsiColor { get => GetText("cmd_noAnsiColor"); } public static string cmd_forceAnsiConsole => GetText("cmd_forceAnsiConsole");
public static string cmd_keys { get => GetText("cmd_keys"); } public static string cmd_noAnsiColor => GetText("cmd_noAnsiColor");
public static string cmd_keyText { get => GetText("cmd_keyText"); } public static string cmd_keys => GetText("cmd_keys");
public static string cmd_loadKeyFailed { get => GetText("cmd_loadKeyFailed"); } public static string cmd_keyText => GetText("cmd_keyText");
public static string cmd_logLevel { get => GetText("cmd_logLevel"); } public static string cmd_loadKeyFailed => GetText("cmd_loadKeyFailed");
public static string cmd_MP4RealTimeDecryption { get => GetText("cmd_MP4RealTimeDecryption"); } public static string cmd_logLevel => GetText("cmd_logLevel");
public static string cmd_saveDir { get => GetText("cmd_saveDir"); } public static string cmd_MP4RealTimeDecryption => GetText("cmd_MP4RealTimeDecryption");
public static string cmd_saveName { get => GetText("cmd_saveName"); } public static string cmd_saveDir => GetText("cmd_saveDir");
public static string cmd_savePattern { get => GetText("cmd_savePattern"); } public static string cmd_saveName => GetText("cmd_saveName");
public static string cmd_skipDownload { get => GetText("cmd_skipDownload"); } public static string cmd_savePattern => GetText("cmd_savePattern");
public static string cmd_noDateInfo { get => GetText("cmd_noDateInfo"); } public static string cmd_skipDownload => GetText("cmd_skipDownload");
public static string cmd_noLog { get => GetText("cmd_noLog"); } public static string cmd_noDateInfo => GetText("cmd_noDateInfo");
public static string cmd_skipMerge { get => GetText("cmd_skipMerge"); } public static string cmd_noLog => GetText("cmd_noLog");
public static string cmd_subFormat { get => GetText("cmd_subFormat"); } public static string cmd_allowHlsMultiExtMap => GetText("cmd_allowHlsMultiExtMap");
public static string cmd_subOnly { get => GetText("cmd_subOnly"); } public static string cmd_skipMerge => GetText("cmd_skipMerge");
public static string cmd_subtitleFix { get => GetText("cmd_subtitleFix"); } public static string cmd_subFormat => GetText("cmd_subFormat");
public static string cmd_threadCount { get => GetText("cmd_threadCount"); } public static string cmd_subOnly => GetText("cmd_subOnly");
public static string cmd_downloadRetryCount { get => GetText("cmd_downloadRetryCount"); } public static string cmd_subtitleFix => GetText("cmd_subtitleFix");
public static string cmd_tmpDir { get => GetText("cmd_tmpDir"); } public static string cmd_threadCount => GetText("cmd_threadCount");
public static string cmd_uiLanguage { get => GetText("cmd_uiLanguage"); } public static string cmd_downloadRetryCount => GetText("cmd_downloadRetryCount");
public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); } public static string cmd_httpRequestTimeout => GetText("cmd_httpRequestTimeout");
public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); } public static string cmd_tmpDir => GetText("cmd_tmpDir");
public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); } public static string cmd_uiLanguage => GetText("cmd_uiLanguage");
public static string cmd_useSystemProxy { get => GetText("cmd_useSystemProxy"); } public static string cmd_urlProcessorArgs => GetText("cmd_urlProcessorArgs");
public static string cmd_customProxy { get => GetText("cmd_customProxy"); } public static string cmd_useShakaPackager => GetText("cmd_useShakaPackager");
public static string cmd_customRange { get => GetText("cmd_customRange"); } public static string cmd_decryptionEngine => GetText("cmd_decryptionEngine");
public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); } public static string cmd_concurrentDownload => GetText("cmd_concurrentDownload");
public static string cmd_livePipeMux { get => GetText("cmd_livePipeMux"); } public static string cmd_useSystemProxy => GetText("cmd_useSystemProxy");
public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); } public static string cmd_customProxy => GetText("cmd_customProxy");
public static string cmd_taskStartAt { get => GetText("cmd_taskStartAt"); } public static string cmd_customRange => GetText("cmd_customRange");
public static string cmd_liveWaitTime { get => GetText("cmd_liveWaitTime"); } public static string cmd_liveKeepSegments => GetText("cmd_liveKeepSegments");
public static string cmd_liveTakeCount { get => GetText("cmd_liveTakeCount"); } public static string cmd_livePipeMux => GetText("cmd_livePipeMux");
public static string cmd_liveFixVttByAudio { get => GetText("cmd_liveFixVttByAudio"); } public static string cmd_liveRecordLimit => GetText("cmd_liveRecordLimit");
public static string cmd_liveRealTimeMerge { get => GetText("cmd_liveRealTimeMerge"); } public static string cmd_taskStartAt => GetText("cmd_taskStartAt");
public static string cmd_livePerformAsVod { get => GetText("cmd_livePerformAsVod"); } public static string cmd_liveWaitTime => GetText("cmd_liveWaitTime");
public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); } public static string cmd_liveTakeCount => GetText("cmd_liveTakeCount");
public static string cmd_muxAfterDone_more { get => GetText("cmd_muxAfterDone_more"); } public static string cmd_liveFixVttByAudio => GetText("cmd_liveFixVttByAudio");
public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); } public static string cmd_liveRealTimeMerge => GetText("cmd_liveRealTimeMerge");
public static string liveLimit { get => GetText("liveLimit"); } public static string cmd_livePerformAsVod => GetText("cmd_livePerformAsVod");
public static string realTimeDecMessage { get => GetText("realTimeDecMessage"); } public static string cmd_muxAfterDone => GetText("cmd_muxAfterDone");
public static string liveLimitReached { get => GetText("liveLimitReached"); } public static string cmd_muxAfterDone_more => GetText("cmd_muxAfterDone_more");
public static string saveName { get => GetText("saveName"); } public static string cmd_writeMetaJson => GetText("cmd_writeMetaJson");
public static string taskStartAt { get => GetText("taskStartAt"); } public static string liveLimit => GetText("liveLimit");
public static string namedPipeCreated { get => GetText("namedPipeCreated"); } public static string realTimeDecMessage => GetText("realTimeDecMessage");
public static string namedPipeMux { get => GetText("namedPipeMux"); } public static string liveLimitReached => GetText("liveLimitReached");
public static string partMerge { get => GetText("partMerge"); } public static string saveName => GetText("saveName");
public static string fetch { get => GetText("fetch"); } public static string taskStartAt => GetText("taskStartAt");
public static string ffmpegMerge { get => GetText("ffmpegMerge"); } public static string namedPipeCreated => GetText("namedPipeCreated");
public static string ffmpegNotFound { get => GetText("ffmpegNotFound"); } public static string namedPipeMux => GetText("namedPipeMux");
public static string fixingTTML { get => GetText("fixingTTML"); } public static string partMerge => GetText("partMerge");
public static string fixingTTMLmp4 { get => GetText("fixingTTMLmp4"); } public static string fetch => GetText("fetch");
public static string fixingVTT { get => GetText("fixingVTT"); } public static string ffmpegMerge => GetText("ffmpegMerge");
public static string fixingVTTmp4 { get => GetText("fixingVTTmp4"); } public static string ffmpegNotFound => GetText("ffmpegNotFound");
public static string keyProcessorNotFound { get => GetText("keyProcessorNotFound"); } public static string mkvmergeNotFound => GetText("mkvmergeNotFound");
public static string liveFound { get => GetText("liveFound"); } public static string mp4decryptNotFound => GetText("mp4decryptNotFound");
public static string loadingUrl { get => GetText("loadingUrl"); } public static string shakaPackagerNotFound => GetText("shakaPackagerNotFound");
public static string masterM3u8Found { get => GetText("masterM3u8Found"); } public static string fixingTTML => GetText("fixingTTML");
public static string matchDASH { get => GetText("matchDASH"); } public static string fixingTTMLmp4 => GetText("fixingTTMLmp4");
public static string matchMSS { get => GetText("matchMSS"); } public static string fixingVTT => GetText("fixingVTT");
public static string matchTS { get => GetText("matchTS"); } public static string fixingVTTmp4 => GetText("fixingVTTmp4");
public static string matchHLS { get => GetText("matchHLS"); } public static string keyProcessorNotFound => GetText("keyProcessorNotFound");
public static string notSupported { get => GetText("notSupported"); } public static string liveFound => GetText("liveFound");
public static string parsingStream { get => GetText("parsingStream"); } public static string loadingUrl => GetText("loadingUrl");
public static string promptChoiceText { get => GetText("promptChoiceText"); } public static string masterM3u8Found => GetText("masterM3u8Found");
public static string promptInfo { get => GetText("promptInfo"); } public static string allowHlsMultiExtMap => GetText("allowHlsMultiExtMap");
public static string promptTitle { get => GetText("promptTitle"); } public static string matchDASH => GetText("matchDASH");
public static string readingInfo { get => GetText("readingInfo"); } public static string matchMSS => GetText("matchMSS");
public static string searchKey { get => GetText("searchKey"); } public static string matchTS => GetText("matchTS");
public static string segmentCountCheckNotPass { get => GetText("segmentCountCheckNotPass"); } public static string matchHLS => GetText("matchHLS");
public static string selectedStream { get => GetText("selectedStream"); } public static string notSupported => GetText("notSupported");
public static string startDownloading { get => GetText("startDownloading"); } public static string parsingStream => GetText("parsingStream");
public static string streamsInfo { get => GetText("streamsInfo"); } public static string promptChoiceText => GetText("promptChoiceText");
public static string writeJson { get => GetText("writeJson"); } public static string promptInfo => GetText("promptInfo");
public static string noStreamsToDownload { get => GetText("noStreamsToDownload"); } public static string promptTitle => GetText("promptTitle");
public static string newVersionFound { get => GetText("newVersionFound"); } public static string readingInfo => GetText("readingInfo");
public static string processImageSub { get => GetText("processImageSub"); } public static string searchKey => GetText("searchKey");
public static string decryptionFailed => GetText("decryptionFailed");
public static string segmentCountCheckNotPass => GetText("segmentCountCheckNotPass");
public static string selectedStream => GetText("selectedStream");
public static string startDownloading => GetText("startDownloading");
public static string streamsInfo => GetText("streamsInfo");
public static string writeJson => GetText("writeJson");
public static string noStreamsToDownload => GetText("noStreamsToDownload");
public static string newVersionFound => GetText("newVersionFound");
public static string processImageSub => GetText("processImageSub");
private static string GetText(string key) private static string GetText(string key)
{ {
if (!StaticText.LANG_DIC.ContainsKey(key)) if (!StaticText.LANG_DIC.TryGetValue(key, out var textObj))
return "<...LANG TEXT MISSING...>"; return "<...LANG TEXT MISSING...>";
var current = Thread.CurrentThread.CurrentUICulture.Name; if (CurrentLoc is "zh-CN" or "zh-SG" or "zh-Hans")
if (current == "zh-CN" || current == "zh-SG" || current == "zh-Hans") return textObj.ZH_CN;
return StaticText.LANG_DIC[key].ZH_CN; return CurrentLoc.StartsWith("zh-") ? textObj.ZH_TW : textObj.EN_US;
else if (current.StartsWith("zh-"))
return StaticText.LANG_DIC[key].ZH_TW;
else
return StaticText.LANG_DIC[key].EN_US;
}
} }
} }

View File

@ -1,14 +1,8 @@
using System; namespace N_m3u8DL_RE.Common.Resource;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Resource internal static class StaticText
{ {
internal class StaticText public static readonly Dictionary<string, TextContainer> LANG_DIC = new()
{
public static Dictionary<string, TextContainer> LANG_DIC = new()
{ {
["singleFileSplitWarn"] = new TextContainer ["singleFileSplitWarn"] = new TextContainer
( (
@ -172,6 +166,12 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "關閉日誌文件輸出", zhTW: "關閉日誌文件輸出",
enUS: "Disable log file output" enUS: "Disable log file output"
), ),
["cmd_allowHlsMultiExtMap"] = new TextContainer
(
zhCN: "允许HLS中的多个#EXT-X-MAP(实验性)",
zhTW: "允許HLS中的多個#EXT-X-MAP(實驗性)",
enUS: "Allow multiple #EXT-X-MAP in HLS (experimental)"
),
["cmd_appendUrlParams"] = new TextContainer ["cmd_appendUrlParams"] = new TextContainer
( (
zhCN: "将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com", zhCN: "将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com",
@ -184,6 +184,12 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "自動選擇所有類型的最佳軌道", zhTW: "自動選擇所有類型的最佳軌道",
enUS: "Automatically selects the best tracks of all types" enUS: "Automatically selects the best tracks of all types"
), ),
["cmd_disableUpdateCheck"] = new TextContainer
(
zhCN: "禁用版本更新检测",
zhTW: "禁用版本更新檢測",
enUS: "Disable version update check"
),
["cmd_binaryMerge"] = new TextContainer ["cmd_binaryMerge"] = new TextContainer
( (
zhCN: "二进制合并", zhCN: "二进制合并",
@ -208,11 +214,17 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "每個分片下載異常時的重試次數", zhTW: "每個分片下載異常時的重試次數",
enUS: "The number of retries when download segment error" enUS: "The number of retries when download segment error"
), ),
["cmd_httpRequestTimeout"] = new TextContainer
(
zhCN: "HTTP请求的超时时间(秒)",
zhTW: "HTTP請求的超時時間(秒)",
enUS: "Timeout duration for HTTP requests (in seconds)"
),
["cmd_decryptionBinaryPath"] = new TextContainer ["cmd_decryptionBinaryPath"] = new TextContainer
( (
zhCN: "MP4解密所用工具的全路径, 例如 C:\\Tools\\mp4decrypt.exe", zhCN: @"MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe",
zhTW: "MP4解密所用工具的全路徑, 例如 C:\\Tools\\mp4decrypt.exe", zhTW: @"MP4解密所用工具的全路徑, 例如 C:\Tools\mp4decrypt.exe",
enUS: "Full path to the tool used for MP4 decryption, like C:\\Tools\\mp4decrypt.exe" enUS: @"Full path to the tool used for MP4 decryption, like C:\Tools\mp4decrypt.exe"
), ),
["cmd_delAfterDone"] = new TextContainer ["cmd_delAfterDone"] = new TextContainer
( (
@ -222,15 +234,15 @@ namespace N_m3u8DL_RE.Common.Resource
), ),
["cmd_ffmpegBinaryPath"] = new TextContainer ["cmd_ffmpegBinaryPath"] = new TextContainer
( (
zhCN: "ffmpeg可执行程序全路径, 例如 C:\\Tools\\ffmpeg.exe", zhCN: @"ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe",
zhTW: "ffmpeg可執行程序全路徑, 例如 C:\\Tools\\ffmpeg.exe", zhTW: @"ffmpeg可執行程序全路徑, 例如 C:\Tools\ffmpeg.exe",
enUS: "Full path to the ffmpeg binary, like C:\\Tools\\ffmpeg.exe" enUS: @"Full path to the ffmpeg binary, like C:\Tools\ffmpeg.exe"
), ),
["cmd_mkvmergeBinaryPath"] = new TextContainer ["cmd_mkvmergeBinaryPath"] = new TextContainer
( (
zhCN: "mkvmerge可执行程序全路径, 例如 C:\\Tools\\mkvmerge.exe", zhCN: @"mkvmerge可执行程序全路径, 例如 C:\Tools\mkvmerge.exe",
zhTW: "mkvmerge可執行程序全路徑, 例如 C:\\Tools\\mkvmerge.exe", zhTW: @"mkvmerge可執行程序全路徑, 例如 C:\Tools\mkvmerge.exe",
enUS: "Full path to the mkvmerge binary, like C:\\Tools\\mkvmerge.exe" enUS: @"Full path to the mkvmerge binary, like C:\Tools\mkvmerge.exe"
), ),
["cmd_liveFixVttByAudio"] = new TextContainer ["cmd_liveFixVttByAudio"] = new TextContainer
( (
@ -252,9 +264,9 @@ namespace N_m3u8DL_RE.Common.Resource
), ),
["cmd_keys"] = new TextContainer ["cmd_keys"] = new TextContainer
( (
zhCN: "设置解密密钥, 程序调用mp4decrpyt/shaka-packager进行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2", zhCN: "设置解密密钥, 程序调用mp4decrpyt/shaka-packager/ffmpeg进行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2\r\n对于KEY相同的情况可以直接输入 --key KEY",
zhTW: "設置解密密鑰, 程序調用mp4decrpyt/shaka-packager進行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2", zhTW: "設置解密密鑰, 程序調用mp4decrpyt/shaka-packager/ffmpeg進行解密. 格式:\r\n--key KID1:KEY1 --key KID2:KEY2\r\n對於KEY相同的情況可以直接輸入 --key KEY",
enUS: "Pass decryption key(s) to mp4decrypt/shaka-packager. format:\r\n--key KID1:KEY1 --key KID2:KEY2" 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."
), ),
["cmd_keyText"] = new TextContainer ["cmd_keyText"] = new TextContainer
( (
@ -454,6 +466,12 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "解密時使用shaka-packager替代mp4decrypt", zhTW: "解密時使用shaka-packager替代mp4decrypt",
enUS: "Use shaka-packager instead of mp4decrypt to decrypt" enUS: "Use shaka-packager instead of mp4decrypt to decrypt"
), ),
["cmd_decryptionEngine"] = new TextContainer
(
zhCN: "设置解密时使用的第三方程序",
zhTW: "設置解密時使用的第三方程序",
enUS: "Set the third-party program used for decryption"
),
["cmd_concurrentDownload"] = new TextContainer ["cmd_concurrentDownload"] = new TextContainer
( (
zhCN: "并发下载已选择的音频、视频和字幕", zhCN: "并发下载已选择的音频、视频和字幕",
@ -631,7 +649,7 @@ namespace N_m3u8DL_RE.Common.Resource
["cmd_muxAfterDone_more"] = new TextContainer ["cmd_muxAfterDone_more"] = new TextContainer
( (
zhCN: "所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\r\n\r\n" + zhCN: "所有工作完成时尝试混流分离的音视频. 你能够以:分隔形式指定如下参数:\r\n\r\n" +
"* format=FORMAT: 指定混流容器 mkv, mp4\r\n" + "* format=FORMAT: 指定混流容器 mkv, mp4, ts\r\n" +
"* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\r\n" + "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默认: ffmpeg)\r\n" +
"* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" + "* bin_path=PATH: 指定程序路径 (默认: 自动寻找)\r\n" +
"* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\r\n" + "* skip_sub=BOOL: 是否忽略字幕文件 (默认: false)\r\n" +
@ -644,7 +662,7 @@ namespace N_m3u8DL_RE.Common.Resource
"# 使用mkvmerge, 自定义程序路径\r\n" + "# 使用mkvmerge, 自定义程序路径\r\n" +
"-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n",
zhTW: "所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\r\n\r\n" + zhTW: "所有工作完成時嘗試混流分離的影音. 你能夠以:分隔形式指定如下參數:\r\n\r\n" +
"* format=FORMAT: 指定混流容器 mkv, mp4\r\n" + "* format=FORMAT: 指定混流容器 mkv, mp4, ts\r\n" +
"* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默認: ffmpeg)\r\n" + "* muxer=MUXER: 指定混流程序 ffmpeg, mkvmerge (默認: ffmpeg)\r\n" +
"* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" + "* bin_path=PATH: 指定程序路徑 (默認: 自動尋找)\r\n" +
"* skip_sub=BOOL: 是否忽略字幕文件 (默認: false)\r\n" + "* skip_sub=BOOL: 是否忽略字幕文件 (默認: false)\r\n" +
@ -657,7 +675,7 @@ namespace N_m3u8DL_RE.Common.Resource
"# 使用mkvmerge, 自訂程序路徑\r\n" + "# 使用mkvmerge, 自訂程序路徑\r\n" +
"-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n", "-M format=mkv:muxer=mkvmerge:bin_path=\"C\\:\\Program Files\\MKVToolNix\\mkvmerge.exe\"\r\n",
enUS: "When all works is done, try to mux the downloaded streams. OPTIONS is a colon separated list of:\r\n\r\n" + enUS: "When all works is done, try to mux the downloaded streams. OPTIONS is a colon separated list of:\r\n\r\n" +
"* format=FORMAT: set container. mkv, mp4\r\n" + "* format=FORMAT: set container. mkv, mp4, ts\r\n" +
"* muxer=MUXER: set muxer. ffmpeg, mkvmerge (Default: ffmpeg)\r\n" + "* muxer=MUXER: set muxer. ffmpeg, mkvmerge (Default: ffmpeg)\r\n" +
"* bin_path=PATH: set binary file path. (Default: auto)\r\n" + "* bin_path=PATH: set binary file path. (Default: auto)\r\n" +
"* skip_sub=BOOL: set whether or not skip subtitle files (Default: false)\r\n" + "* skip_sub=BOOL: set whether or not skip subtitle files (Default: false)\r\n" +
@ -726,9 +744,9 @@ namespace N_m3u8DL_RE.Common.Resource
), ),
["realTimeDecMessage"] = new TextContainer ["realTimeDecMessage"] = new TextContainer
( (
zhCN: "启用实时解密时建议用shaka-packager而非mp4decrypt", zhCN: "启用实时解密时建议用shaka-packager而非mp4decrypt/ffmpeg",
zhTW: "啟用即時解密時建議用shaka-packager而非mp4decrypt", zhTW: "啟用即時解密時建議用shaka-packager而非mp4decrypt/ffmpeg",
enUS: "When enabling real-time decryption, it is recommended to use shaka-packager instead of mp4decrypt" enUS: "When enabling real-time decryption, it is recommended to use shaka-packager instead of mp4decrypt/ffmpeg"
), ),
["liveLimitReached"] = new TextContainer ["liveLimitReached"] = new TextContainer
( (
@ -760,6 +778,24 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "找不到ffmpeg請自行下載https://ffmpeg.org/download.html", zhTW: "找不到ffmpeg請自行下載https://ffmpeg.org/download.html",
enUS: "ffmpeg not found, please download at: https://ffmpeg.org/download.html" enUS: "ffmpeg not found, please download at: https://ffmpeg.org/download.html"
), ),
["mkvmergeNotFound"] = new TextContainer
(
zhCN: "找不到mkvmerge请自行下载https://mkvtoolnix.download/downloads.html",
zhTW: "找不到mkvmerge請自行下載https://mkvtoolnix.download/downloads.html",
enUS: "mkvmerge not found, please download at: https://mkvtoolnix.download/downloads.html"
),
["shakaPackagerNotFound"] = new TextContainer
(
zhCN: "找不到shaka-packager请自行下载https://github.com/shaka-project/shaka-packager/releases",
zhTW: "找不到shaka-packager請自行下載https://github.com/shaka-project/shaka-packager/releases",
enUS: "shaka-packager not found, please download at: https://github.com/shaka-project/shaka-packager/releases"
),
["mp4decryptNotFound"] = new TextContainer
(
zhCN: "找不到mp4decrypt请自行下载https://www.bento4.com/downloads/",
zhTW: "找不到mp4decrypt請自行下載https://www.bento4.com/downloads/",
enUS: "mp4decrypt not found, please download at: https://www.bento4.com/downloads/"
),
["fixingTTML"] = new TextContainer ["fixingTTML"] = new TextContainer
( (
zhCN: "正在提取TTML(raw)字幕...", zhCN: "正在提取TTML(raw)字幕...",
@ -808,6 +844,12 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "檢測到Master列表開始解析全部流訊息", zhTW: "檢測到Master列表開始解析全部流訊息",
enUS: "Master List detected, try parse all streams" enUS: "Master List detected, try parse all streams"
), ),
["allowHlsMultiExtMap"] = new TextContainer
(
zhCN: "已经允许识别多个#EXT-X-MAP标签, 本软件可能无法正确处理, 请手动确认内容完整性",
zhTW: "已經允許識別多個#EXT-X-MAP標籤, 本軟件可能無法正確處理, 請手動確認內容完整性",
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"
),
["matchTS"] = new TextContainer ["matchTS"] = new TextContainer
( (
zhCN: "内容匹配: [white on green3]HTTP Live MPEG2-TS[/]", zhCN: "内容匹配: [white on green3]HTTP Live MPEG2-TS[/]",
@ -880,6 +922,12 @@ namespace N_m3u8DL_RE.Common.Resource
zhTW: "正在嘗試從文本文件搜尋KEY...", zhTW: "正在嘗試從文本文件搜尋KEY...",
enUS: "Trying to search for KEY from text file..." enUS: "Trying to search for KEY from text file..."
), ),
["decryptionFailed"] = new TextContainer
(
zhCN: "解密失败",
zhTW: "解密失敗",
enUS: "Decryption failed"
),
["segmentCountCheckNotPass"] = new TextContainer ["segmentCountCheckNotPass"] = new TextContainer
( (
zhCN: "分片数量校验不通过, 共{}个,已下载{}.", zhCN: "分片数量校验不通过, 共{}个,已下载{}.",
@ -918,5 +966,4 @@ namespace N_m3u8DL_RE.Common.Resource
), ),
}; };
}
} }

View File

@ -1,16 +1,10 @@
using System; namespace N_m3u8DL_RE.Common.Resource;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Resource internal class TextContainer
{ {
internal class TextContainer public string ZH_CN { get; }
{ public string ZH_TW { get; }
public string ZH_CN { get; set; } public string EN_US { get; }
public string ZH_TW { get; set; }
public string EN_US { get; set; }
public TextContainer(string zhCN, string zhTW, string enUS) public TextContainer(string zhCN, string zhTW, string enUS)
{ {
@ -18,5 +12,4 @@ namespace N_m3u8DL_RE.Common.Resource
ZH_TW = zhTW; ZH_TW = zhTW;
EN_US = enUS; EN_US = enUS;
} }
}
} }

View File

@ -1,18 +1,13 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.JsonConverter; using N_m3u8DL_RE.Common.JsonConverter;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Util namespace N_m3u8DL_RE.Common.Util;
public static class GlobalUtil
{ {
public class GlobalUtil private static readonly JsonSerializerOptions Options = new()
{
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{ {
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
WriteIndented = true, WriteIndented = true,
@ -27,15 +22,15 @@ namespace N_m3u8DL_RE.Common.Util
{ {
return JsonSerializer.Serialize(s, Context.StreamSpec); return JsonSerializer.Serialize(s, Context.StreamSpec);
} }
else if (o is IOrderedEnumerable<StreamSpec> ss) if (o is IOrderedEnumerable<StreamSpec> ss)
{ {
return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec); return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);
} }
else if (o is List<StreamSpec> sList) if (o is List<StreamSpec> sList)
{ {
return JsonSerializer.Serialize(sList, Context.ListStreamSpec); return JsonSerializer.Serialize(sList, Context.ListStreamSpec);
} }
else if (o is IEnumerable<MediaSegment> mList) if (o is IEnumerable<MediaSegment> mList)
{ {
return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment); return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment);
} }
@ -47,14 +42,14 @@ namespace N_m3u8DL_RE.Common.Util
return fileSize switch return fileSize switch
{ {
< 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)), < 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),
>= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GB", (double)fileSize / (1024 * 1024 * 1024)), >= 1024 * 1024 * 1024 => $"{fileSize / (1024 * 1024 * 1024):########0.00}GB",
>= 1024 * 1024 => string.Format("{0:####0.00}MB", (double)fileSize / (1024 * 1024)), >= 1024 * 1024 => $"{fileSize / (1024 * 1024):####0.00}MB",
>= 1024 => string.Format("{0:####0.00}KB", (double)fileSize / 1024), >= 1024 => $"{fileSize / 1024:####0.00}KB",
_ => string.Format("{0:####0.00}B", fileSize) _ => $"{fileSize:####0.00}B"
}; };
} }
//此函数用于格式化输出时长 // 此函数用于格式化输出时长
public static string FormatTime(int time) public static string FormatTime(int time)
{ {
TimeSpan ts = new TimeSpan(0, 0, time); TimeSpan ts = new TimeSpan(0, 0, time);
@ -72,9 +67,7 @@ namespace N_m3u8DL_RE.Common.Util
{ {
var fileExt = OperatingSystem.IsWindows() ? ".exe" : ""; var fileExt = OperatingSystem.IsWindows() ? ".exe" : "";
var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) }; var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) };
var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
Array.Empty<string>(); return searchPath.Concat(envPath).Select(p => Path.Combine(p!, name + fileExt)).FirstOrDefault(File.Exists);
return searchPath.Concat(envPath).Select(p => Path.Combine(p, name + fileExt)).FirstOrDefault(File.Exists);
}
} }
} }

View File

@ -3,10 +3,10 @@ using System.Net.Http.Headers;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
namespace N_m3u8DL_RE.Common.Util namespace N_m3u8DL_RE.Common.Util;
public static class HTTPUtil
{ {
public class HTTPUtil
{
public static readonly HttpClientHandler HttpClientHandler = new() public static readonly HttpClientHandler HttpClientHandler = new()
{ {
AllowAutoRedirect = false, AllowAutoRedirect = false,
@ -37,13 +37,13 @@ namespace N_m3u8DL_RE.Common.Util
} }
} }
Logger.Debug(webRequest.Headers.ToString()); Logger.Debug(webRequest.Headers.ToString());
//手动处理跳转以免自定义Headers丢失 // 手动处理跳转以免自定义Headers丢失
var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead); var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead);
if (((int)webResponse.StatusCode).ToString().StartsWith("30")) if (((int)webResponse.StatusCode).ToString().StartsWith("30"))
{ {
HttpResponseHeaders respHeaders = webResponse.Headers; HttpResponseHeaders respHeaders = webResponse.Headers;
Logger.Debug(respHeaders.ToString()); Logger.Debug(respHeaders.ToString());
if (respHeaders != null && respHeaders.Location != null) if (respHeaders.Location != null)
{ {
var redirectedUrl = ""; var redirectedUrl = "";
if (!respHeaders.Location.IsAbsoluteUri) if (!respHeaders.Location.IsAbsoluteUri)
@ -64,7 +64,7 @@ namespace N_m3u8DL_RE.Common.Util
} }
} }
} }
//手动将跳转后的URL设置进去, 用于后续取用 // 手动将跳转后的URL设置进去, 用于后续取用
webResponse.Headers.Location = new Uri(url); webResponse.Headers.Location = new Uri(url);
webResponse.EnsureSuccessStatusCode(); webResponse.EnsureSuccessStatusCode();
return webResponse; return webResponse;
@ -76,9 +76,8 @@ namespace N_m3u8DL_RE.Common.Util
{ {
return await File.ReadAllBytesAsync(new Uri(url).LocalPath); return await File.ReadAllBytesAsync(new Uri(url).LocalPath);
} }
byte[] bytes = new byte[0];
var webResponse = await DoGetAsync(url, headers); var webResponse = await DoGetAsync(url, headers);
bytes = await webResponse.Content.ReadAsByteArrayAsync(); var bytes = await webResponse.Content.ReadAsByteArrayAsync();
Logger.Debug(HexUtil.BytesToHex(bytes, " ")); Logger.Debug(HexUtil.BytesToHex(bytes, " "));
return bytes; return bytes;
} }
@ -91,9 +90,8 @@ namespace N_m3u8DL_RE.Common.Util
/// <returns></returns> /// <returns></returns>
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null) public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
{ {
string htmlCode = string.Empty;
var webResponse = await DoGetAsync(url, headers); var webResponse = await DoGetAsync(url, headers);
htmlCode = await webResponse.Content.ReadAsStringAsync(); string htmlCode = await webResponse.Content.ReadAsStringAsync();
Logger.Debug(htmlCode); Logger.Debug(htmlCode);
return htmlCode; return htmlCode;
} }
@ -101,7 +99,7 @@ namespace N_m3u8DL_RE.Common.Util
private static bool CheckMPEG2TS(HttpResponseMessage? webResponse) private static bool CheckMPEG2TS(HttpResponseMessage? webResponse)
{ {
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower(); var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower();
return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg"; return mediaType is "video/ts" or "video/mp2t" or "video/mpeg";
} }
/// <summary> /// <summary>
@ -112,7 +110,7 @@ namespace N_m3u8DL_RE.Common.Util
/// <returns>(Source Code, RedirectedUrl)</returns> /// <returns>(Source Code, RedirectedUrl)</returns>
public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null) public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null)
{ {
string htmlCode = string.Empty; string htmlCode;
var webResponse = await DoGetAsync(url, headers); var webResponse = await DoGetAsync(url, headers);
if (CheckMPEG2TS(webResponse)) if (CheckMPEG2TS(webResponse))
{ {
@ -128,7 +126,7 @@ namespace N_m3u8DL_RE.Common.Util
public static async Task<string> GetPostResponseAsync(string Url, byte[] postData) public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)
{ {
string htmlCode = string.Empty; string htmlCode;
using HttpRequestMessage request = new(HttpMethod.Post, Url); using HttpRequestMessage request = new(HttpMethod.Post, Url);
request.Headers.TryAddWithoutValidation("Content-Type", "application/json"); request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString()); request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());
@ -137,5 +135,4 @@ namespace N_m3u8DL_RE.Common.Util
htmlCode = await webResponse.Content.ReadAsStringAsync(); htmlCode = await webResponse.Content.ReadAsStringAsync();
return htmlCode; return htmlCode;
} }
}
} }

View File

@ -1,13 +1,7 @@
using System; namespace N_m3u8DL_RE.Common.Util;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Common.Util public static class HexUtil
{ {
public class HexUtil
{
public static string BytesToHex(byte[] data, string split = "") public static string BytesToHex(byte[] data, string split = "")
{ {
return BitConverter.ToString(data).Replace("-", split); return BitConverter.ToString(data).Replace("-", split);
@ -34,15 +28,12 @@ namespace N_m3u8DL_RE.Common.Util
public static byte[] HexToBytes(string hex) public static byte[] HexToBytes(string hex)
{ {
hex = hex.Trim(); var hexSpan = hex.AsSpan().Trim();
if (hex.StartsWith("0x") || hex.StartsWith("0X")) if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X"))
hex = hex.Substring(2); {
byte[] bytes = new byte[hex.Length / 2]; hexSpan = hexSpan[2..];
for (int i = 0; i < hex.Length; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
} }
return Convert.FromHexString(hexSpan);
} }
} }

View File

@ -4,7 +4,7 @@ using Spectre.Console;
namespace N_m3u8DL_RE.Common.Util; namespace N_m3u8DL_RE.Common.Util;
public class RetryUtil public static class RetryUtil
{ {
public static async Task<T?> WebRequestRetryAsync<T>(Func<Task<T>> funcAsync, int maxRetries = 10, int retryDelayMilliseconds = 1500, int retryDelayIncrementMilliseconds = 0) public static async Task<T?> WebRequestRetryAsync<T>(Func<Task<T>> funcAsync, int maxRetries = 10, int retryDelayMilliseconds = 1500, int retryDelayIncrementMilliseconds = 0)
{ {

View File

@ -3,17 +3,19 @@ using N_m3u8DL_RE.Parser.Processor;
using N_m3u8DL_RE.Parser.Processor.DASH; using N_m3u8DL_RE.Parser.Processor.DASH;
using N_m3u8DL_RE.Parser.Processor.HLS; using N_m3u8DL_RE.Parser.Processor.HLS;
namespace N_m3u8DL_RE.Parser.Config namespace N_m3u8DL_RE.Parser.Config;
public class ParserConfig
{ {
public class ParserConfig public string Url { get; set; } = string.Empty;
{
public string Url { get; set; }
public string OriginalUrl { get; set; } public string OriginalUrl { get; set; } = string.Empty;
public string BaseUrl { get; set; } public string BaseUrl { get; set; } = string.Empty;
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); public Dictionary<string, string> CustomParserArgs { get; } = new();
public Dictionary<string, string> Headers { get; init; } = new();
/// <summary> /// <summary>
/// 内容前置处理器. 调用顺序与列表顺序相同 /// 内容前置处理器. 调用顺序与列表顺序相同
@ -64,5 +66,4 @@ namespace N_m3u8DL_RE.Parser.Config
/// KEY重试次数 /// KEY重试次数
/// </summary> /// </summary>
public int KeyRetryCount { get; set; } = 3; public int KeyRetryCount { get; set; } = 3;
}
} }

View File

@ -1,16 +1,9 @@
using System; namespace N_m3u8DL_RE.Parser.Constants;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants internal static class DASHTags
{ {
internal class DASHTags public const string TemplateRepresentationID = "$RepresentationID$";
{ public const string TemplateBandwidth = "$Bandwidth$";
public static string TemplateRepresentationID = "$RepresentationID$"; public const string TemplateNumber = "$Number$";
public static string TemplateBandwidth = "$Bandwidth$"; public const string TemplateTime = "$Time$";
public static string TemplateNumber = "$Number$";
public static string TemplateTime = "$Time$";
}
} }

View File

@ -1,38 +1,31 @@
using System; namespace N_m3u8DL_RE.Parser.Constants;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants internal static class HLSTags
{ {
internal class HLSTags public const string ext_m3u = "#EXTM3U";
{ public const string ext_x_targetduration = "#EXT-X-TARGETDURATION";
public static string ext_m3u = "#EXTM3U"; public const string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE";
public static string ext_x_targetduration = "#EXT-X-TARGETDURATION"; public const string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE";
public static string ext_x_media_sequence = "#EXT-X-MEDIA-SEQUENCE"; public const string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME";
public static string ext_x_discontinuity_sequence = "#EXT-X-DISCONTINUITY-SEQUENCE"; public const string ext_x_media = "#EXT-X-MEDIA";
public static string ext_x_program_date_time = "#EXT-X-PROGRAM-DATE-TIME"; public const string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE";
public static string ext_x_media = "#EXT-X-MEDIA"; public const string ext_x_key = "#EXT-X-KEY";
public static string ext_x_playlist_type = "#EXT-X-PLAYLIST-TYPE"; public const string ext_x_stream_inf = "#EXT-X-STREAM-INF";
public static string ext_x_key = "#EXT-X-KEY"; public const string ext_x_version = "#EXT-X-VERSION";
public static string ext_x_stream_inf = "#EXT-X-STREAM-INF"; public const string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE";
public static string ext_x_version = "#EXT-X-VERSION"; public const string ext_x_endlist = "#EXT-X-ENDLIST";
public static string ext_x_allow_cache = "#EXT-X-ALLOW-CACHE"; public const string extinf = "#EXTINF";
public static string ext_x_endlist = "#EXT-X-ENDLIST"; public const string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY";
public static string extinf = "#EXTINF"; public const string ext_x_byterange = "#EXT-X-BYTERANGE";
public static string ext_i_frames_only = "#EXT-X-I-FRAMES-ONLY"; public const string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF";
public static string ext_x_byterange = "#EXT-X-BYTERANGE"; public const string ext_x_discontinuity = "#EXT-X-DISCONTINUITY";
public static string ext_x_i_frame_stream_inf = "#EXT-X-I-FRAME-STREAM-INF"; public const string ext_x_cue_out_start = "#EXT-X-CUE-OUT";
public static string ext_x_discontinuity = "#EXT-X-DISCONTINUITY"; public const string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT";
public static string ext_x_cue_out_start = "#EXT-X-CUE-OUT"; public const string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS";
public static string ext_x_cue_out = "#EXT-X-CUE-OUT-CONT"; public const string ext_x_scte35 = "#EXT-OATCLS-SCTE35";
public static string ext_is_independent_segments = "#EXT-X-INDEPENDENT-SEGMENTS"; public const string ext_x_cue_start = "#EXT-X-CUE-OUT";
public static string ext_x_scte35 = "#EXT-OATCLS-SCTE35"; public const string ext_x_cue_end = "#EXT-X-CUE-IN";
public static string ext_x_cue_start = "#EXT-X-CUE-OUT"; public const string ext_x_cue_span = "#EXT-X-CUE-SPAN";
public static string ext_x_cue_end = "#EXT-X-CUE-IN"; public const string ext_x_map = "#EXT-X-MAP";
public static string ext_x_cue_span = "#EXT-X-CUE-SPAN"; public const string ext_x_start = "#EXT-X-START";
public static string ext_x_map = "#EXT-X-MAP";
public static string ext_x_start = "#EXT-X-START";
}
} }

View File

@ -1,16 +1,9 @@
using System; namespace N_m3u8DL_RE.Parser.Constants;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Constants internal static class MSSTags
{ {
internal class MSSTags public const string Bitrate = "{Bitrate}";
{ public const string Bitrate_BK = "{bitrate}";
public static string Bitrate = "{Bitrate}"; public const string StartTime = "{start_time}";
public static string Bitrate_BK = "{bitrate}"; public const string StartTime_BK = "{start time}";
public static string StartTime = "{start_time}";
public static string StartTime_BK = "{start time}";
}
} }

View File

@ -4,20 +4,15 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
// https://blog.csdn.net/leek5533/article/details/117750191
internal partial class DASHExtractor2 : IExtractor
{ {
//https://blog.csdn.net/leek5533/article/details/117750191
internal class DASHExtractor2 : IExtractor
{
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC; private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;
public ExtractorType ExtractorType => ExtractorType.MPEG_DASH; public ExtractorType ExtractorType => ExtractorType.MPEG_DASH;
@ -37,15 +32,12 @@ namespace N_m3u8DL_RE.Parser.Extractor
private void SetInitUrl() private void SetInitUrl()
{ {
this.MpdUrl = ParserConfig.Url ?? string.Empty; this.MpdUrl = ParserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.MpdUrl;
this.BaseUrl = ParserConfig.BaseUrl;
else
this.BaseUrl = this.MpdUrl;
} }
private string ExtendBaseUrl(XElement element, string oriBaseUrl) private string ExtendBaseUrl(XElement element, string oriBaseUrl)
{ {
var target = element.Elements().Where(e => e.Name.LocalName == "BaseURL").FirstOrDefault(); var target = element.Elements().FirstOrDefault(e => e.Name.LocalName == "BaseURL");
if (target != null) if (target != null)
{ {
oriBaseUrl = ParserUtil.CombineURL(oriBaseUrl, target.Value); oriBaseUrl = ParserUtil.CombineURL(oriBaseUrl, target.Value);
@ -57,16 +49,14 @@ namespace N_m3u8DL_RE.Parser.Extractor
private double? GetFrameRate(XElement element) private double? GetFrameRate(XElement element)
{ {
var frameRate = element.Attribute("frameRate")?.Value; var frameRate = element.Attribute("frameRate")?.Value;
if (frameRate != null && frameRate.Contains("/")) if (frameRate == null || !frameRate.Contains('/')) return null;
{
var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]); var d = Convert.ToDouble(frameRate.Split('/')[0]) / Convert.ToDouble(frameRate.Split('/')[1]);
frameRate = d.ToString("0.000"); frameRate = d.ToString("0.000");
return Convert.ToDouble(frameRate); return Convert.ToDouble(frameRate);
} }
return null;
}
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText) public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{ {
var streamList = new List<StreamSpec>(); var streamList = new List<StreamSpec>();
@ -76,31 +66,31 @@ namespace N_m3u8DL_RE.Parser.Extractor
var xmlDocument = XDocument.Parse(MpdContent); var xmlDocument = XDocument.Parse(MpdContent);
//选中第一个MPD节点 // 选中第一个MPD节点
var mpdElement = xmlDocument.Elements().First(e => e.Name.LocalName == "MPD"); var mpdElement = xmlDocument.Elements().First(e => e.Name.LocalName == "MPD");
//类型 static点播, dynamic直播 // 类型 static点播, dynamic直播
var type = mpdElement.Attribute("type")?.Value; var type = mpdElement.Attribute("type")?.Value;
bool isLive = type == "dynamic"; bool isLive = type == "dynamic";
//分片最大时长 // 分片最大时长
var maxSegmentDuration = mpdElement.Attribute("maxSegmentDuration")?.Value; var maxSegmentDuration = mpdElement.Attribute("maxSegmentDuration")?.Value;
//分片从该时间起可用 // 分片从该时间起可用
var availabilityStartTime = mpdElement.Attribute("availabilityStartTime")?.Value; var availabilityStartTime = mpdElement.Attribute("availabilityStartTime")?.Value;
//在availabilityStartTime的前XX段时间分片有效 // 在availabilityStartTime的前XX段时间分片有效
var timeShiftBufferDepth = mpdElement.Attribute("timeShiftBufferDepth")?.Value; var timeShiftBufferDepth = mpdElement.Attribute("timeShiftBufferDepth")?.Value;
if (string.IsNullOrEmpty(timeShiftBufferDepth)) if (string.IsNullOrEmpty(timeShiftBufferDepth))
{ {
//如果没有 默认一分钟有效 // 如果没有 默认一分钟有效
timeShiftBufferDepth = "PT1M"; timeShiftBufferDepth = "PT1M";
} }
//MPD发布时间 // MPD发布时间
var publishTime = mpdElement.Attribute("publishTime")?.Value; var publishTime = mpdElement.Attribute("publishTime")?.Value;
//MPD总时长 // MPD总时长
var mediaPresentationDuration = mpdElement.Attribute("mediaPresentationDuration")?.Value; var mediaPresentationDuration = mpdElement.Attribute("mediaPresentationDuration")?.Value;
//读取在MPD开头定义的<BaseURL>并替换本身的URL // 读取在MPD开头定义的<BaseURL>并替换本身的URL
var baseUrlElement = mpdElement.Elements().Where(e => e.Name.LocalName == "BaseURL").FirstOrDefault(); var baseUrlElement = mpdElement.Elements().FirstOrDefault(e => e.Name.LocalName == "BaseURL");
if (baseUrlElement != null) if (baseUrlElement != null)
{ {
var baseUrl = baseUrlElement.Value; var baseUrl = baseUrlElement.Value;
@ -108,40 +98,40 @@ namespace N_m3u8DL_RE.Parser.Extractor
this.BaseUrl = ParserUtil.CombineURL(this.MpdUrl, baseUrl); this.BaseUrl = ParserUtil.CombineURL(this.MpdUrl, baseUrl);
} }
//全部Period // 全部Period
var periods = mpdElement.Elements().Where(e => e.Name.LocalName == "Period"); var periods = mpdElement.Elements().Where(e => e.Name.LocalName == "Period");
foreach (var period in periods) foreach (var period in periods)
{ {
//本Period时长 // 本Period时长
var periodDuration = period.Attribute("duration")?.Value; var periodDuration = period.Attribute("duration")?.Value;
//本Period ID // 本Period ID
var periodId = period.Attribute("id")?.Value; var periodId = period.Attribute("id")?.Value;
//最终分片会使用的baseurl // 最终分片会使用的baseurl
var segBaseUrl = this.BaseUrl; var segBaseUrl = this.BaseUrl;
//处理baseurl嵌套 // 处理baseurl嵌套
segBaseUrl = ExtendBaseUrl(period, segBaseUrl); segBaseUrl = ExtendBaseUrl(period, segBaseUrl);
var adaptationSetsBaseUrl = segBaseUrl; var adaptationSetsBaseUrl = segBaseUrl;
//本Period中的全部AdaptationSet // 本Period中的全部AdaptationSet
var adaptationSets = period.Elements().Where(e => e.Name.LocalName == "AdaptationSet"); var adaptationSets = period.Elements().Where(e => e.Name.LocalName == "AdaptationSet");
foreach (var adaptationSet in adaptationSets) foreach (var adaptationSet in adaptationSets)
{ {
//处理baseurl嵌套 // 处理baseurl嵌套
segBaseUrl = ExtendBaseUrl(adaptationSet, segBaseUrl); segBaseUrl = ExtendBaseUrl(adaptationSet, segBaseUrl);
var representationsBaseUrl = segBaseUrl; var representationsBaseUrl = segBaseUrl;
var mimeType = adaptationSet.Attribute("contentType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value; var mimeType = adaptationSet.Attribute("contentType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value;
var frameRate = GetFrameRate(adaptationSet); var frameRate = GetFrameRate(adaptationSet);
//本AdaptationSet中的全部Representation // 本AdaptationSet中的全部Representation
var representations = adaptationSet.Elements().Where(e => e.Name.LocalName == "Representation"); var representations = adaptationSet.Elements().Where(e => e.Name.LocalName == "Representation");
foreach (var representation in representations) foreach (var representation in representations)
{ {
//处理baseurl嵌套 // 处理baseurl嵌套
segBaseUrl = ExtendBaseUrl(representation, segBaseUrl); segBaseUrl = ExtendBaseUrl(representation, segBaseUrl);
if (mimeType == null) if (mimeType == null)
@ -167,29 +157,29 @@ namespace N_m3u8DL_RE.Parser.Extractor
"audio" => MediaType.AUDIO, "audio" => MediaType.AUDIO,
_ => null _ => null
}; };
//特殊处理 // 特殊处理
if (representation.Attribute("volumeAdjust") != null) if (representation.Attribute("volumeAdjust") != null)
{ {
streamSpec.GroupId += "-" + representation.Attribute("volumeAdjust")?.Value; streamSpec.GroupId += "-" + representation.Attribute("volumeAdjust")?.Value;
} }
//推测后缀名 // 推测后缀名
var mType = representation.Attribute("mimeType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value; var mType = representation.Attribute("mimeType")?.Value ?? adaptationSet.Attribute("mimeType")?.Value;
if (mType != null) if (mType != null)
{ {
var mTypeSplit = mType.Split('/'); var mTypeSplit = mType.Split('/');
streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null; streamSpec.Extension = mTypeSplit.Length == 2 ? mTypeSplit[1] : null;
} }
//优化字幕场景识别 // 优化字幕场景识别
if (streamSpec.Codecs == "stpp" || streamSpec.Codecs == "wvtt") if (streamSpec.Codecs is "stpp" or "wvtt")
{ {
streamSpec.MediaType = MediaType.SUBTITLES; streamSpec.MediaType = MediaType.SUBTITLES;
} }
//优化字幕场景识别 // 优化字幕场景识别
var role = representation.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault() ?? adaptationSet.Elements().Where(e => e.Name.LocalName == "Role").FirstOrDefault(); var role = representation.Elements().FirstOrDefault(e => e.Name.LocalName == "Role") ?? adaptationSet.Elements().FirstOrDefault(e => e.Name.LocalName == "Role");
if (role != null) if (role != null)
{ {
var v = role.Attribute("value")?.Value; var roleValue = role.Attribute("value")?.Value;
if (Enum.TryParse(v, true, out RoleType roleType)) if (Enum.TryParse(roleValue, true, out RoleType roleType))
{ {
streamSpec.Role = roleType; streamSpec.Role = roleType;
@ -200,34 +190,49 @@ namespace N_m3u8DL_RE.Parser.Extractor
streamSpec.Extension = "ttml"; streamSpec.Extension = "ttml";
} }
} }
else if (roleValue != null && roleValue.Contains('-'))
{
roleValue = roleValue.Replace("-", "");
if (Enum.TryParse(roleValue, true, out RoleType roleType_))
{
streamSpec.Role = roleType_;
if (roleType_ == RoleType.ForcedSubtitle)
{
streamSpec.MediaType = MediaType.SUBTITLES; // or maybe MediaType.CLOSED_CAPTIONS?
if (mType != null && mType.Contains("ttml"))
streamSpec.Extension = "ttml";
}
}
}
} }
streamSpec.Playlist.IsLive = isLive; streamSpec.Playlist.IsLive = isLive;
//设置刷新间隔 timeShiftBufferDepth / 2 // 设置刷新间隔 timeShiftBufferDepth / 2
if (timeShiftBufferDepth != null) if (timeShiftBufferDepth != null)
{ {
streamSpec.Playlist.RefreshIntervalMs = XmlConvert.ToTimeSpan(timeShiftBufferDepth).TotalMilliseconds / 2; streamSpec.Playlist.RefreshIntervalMs = XmlConvert.ToTimeSpan(timeShiftBufferDepth).TotalMilliseconds / 2;
} }
//读取声道数量 // 读取声道数量
var audioChannelConfiguration = adaptationSet.Elements().Concat(representation.Elements()).Where(e => e.Name.LocalName == "AudioChannelConfiguration").FirstOrDefault(); var audioChannelConfiguration = adaptationSet.Elements().Concat(representation.Elements()).FirstOrDefault(e => e.Name.LocalName == "AudioChannelConfiguration");
if (audioChannelConfiguration != null) if (audioChannelConfiguration != null)
{ {
streamSpec.Channels = audioChannelConfiguration.Attribute("value")?.Value; streamSpec.Channels = audioChannelConfiguration.Attribute("value")?.Value;
} }
//发布时间 // 发布时间
if (!string.IsNullOrEmpty(publishTime)) if (!string.IsNullOrEmpty(publishTime))
{ {
streamSpec.PublishTime = DateTime.Parse(publishTime); streamSpec.PublishTime = DateTime.Parse(publishTime);
} }
//第一种形式 SegmentBase // 第一种形式 SegmentBase
var segmentBaseElement = representation.Elements().Where(e => e.Name.LocalName == "SegmentBase").FirstOrDefault(); var segmentBaseElement = representation.Elements().FirstOrDefault(e => e.Name.LocalName == "SegmentBase");
if (segmentBaseElement != null) if (segmentBaseElement != null)
{ {
//处理init url // 处理init url
var initialization = segmentBaseElement.Elements().Where(e => e.Name.LocalName == "Initialization").FirstOrDefault(); var initialization = segmentBaseElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Initialization");
if (initialization != null) if (initialization != null)
{ {
var sourceURL = initialization.Attribute("sourceURL")?.Value; var sourceURL = initialization.Attribute("sourceURL")?.Value;
@ -248,7 +253,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!); var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!);
var initRange = initialization.Attribute("range")?.Value; var initRange = initialization.Attribute("range")?.Value;
streamSpec.Playlist.MediaInit = new MediaSegment(); streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.Playlist.MediaInit.Index = -1; //便于排序 streamSpec.Playlist.MediaInit.Index = -1; // 便于排序
streamSpec.Playlist.MediaInit.Url = initUrl; streamSpec.Playlist.MediaInit.Url = initUrl;
if (initRange != null) if (initRange != null)
{ {
@ -260,19 +265,19 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
} }
//第二种形式 SegmentList.SegmentList // 第二种形式 SegmentList.SegmentList
var segmentList = representation.Elements().Where(e => e.Name.LocalName == "SegmentList").FirstOrDefault(); var segmentList = representation.Elements().FirstOrDefault(e => e.Name.LocalName == "SegmentList");
if (segmentList != null) if (segmentList != null)
{ {
var durationStr = segmentList.Attribute("duration")?.Value; var durationStr = segmentList.Attribute("duration")?.Value;
//处理init url // 处理init url
var initialization = segmentList.Elements().Where(e => e.Name.LocalName == "Initialization").FirstOrDefault(); var initialization = segmentList.Elements().FirstOrDefault(e => e.Name.LocalName == "Initialization");
if (initialization != null) if (initialization != null)
{ {
var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!); var initUrl = ParserUtil.CombineURL(segBaseUrl, initialization.Attribute("sourceURL")?.Value!);
var initRange = initialization.Attribute("range")?.Value; var initRange = initialization.Attribute("range")?.Value;
streamSpec.Playlist.MediaInit = new MediaSegment(); streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.Playlist.MediaInit.Index = -1; //便于排序 streamSpec.Playlist.MediaInit.Index = -1; // 便于排序
streamSpec.Playlist.MediaInit.Url = initUrl; streamSpec.Playlist.MediaInit.Url = initUrl;
if (initRange != null) if (initRange != null)
{ {
@ -281,10 +286,10 @@ namespace N_m3u8DL_RE.Parser.Extractor
streamSpec.Playlist.MediaInit.ExpectLength = expect; streamSpec.Playlist.MediaInit.ExpectLength = expect;
} }
} }
//处理分片 // 处理分片
var segmentURLs = segmentList.Elements().Where(e => e.Name.LocalName == "SegmentURL"); var segmentURLs = segmentList.Elements().Where(e => e.Name.LocalName == "SegmentURL").ToList();
var timescaleStr = segmentList.Attribute("timescale")?.Value ?? "1"; var timescaleStr = segmentList.Attribute("timescale")?.Value ?? "1";
for (int segmentIndex = 0; segmentIndex < segmentURLs.Count(); segmentIndex++) for (int segmentIndex = 0; segmentIndex < segmentURLs.Count; segmentIndex++)
{ {
var segmentURL = segmentURLs.ElementAt(segmentIndex); var segmentURL = segmentURLs.ElementAt(segmentIndex);
var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute("media")?.Value!); var mediaUrl = ParserUtil.CombineURL(segBaseUrl, segmentURL.Attribute("media")?.Value!);
@ -305,50 +310,50 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
} }
//第三种形式 SegmentTemplate+SegmentTimeline // 第三种形式 SegmentTemplate+SegmentTimeline
//通配符有$RepresentationID$ $Bandwidth$ $Number$ $Time$ // 通配符有$RepresentationID$ $Bandwidth$ $Number$ $Time$
//adaptationSets中的segmentTemplate // adaptationSets中的segmentTemplate
var segmentTemplateElementsOuter = adaptationSet.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); var segmentTemplateElementsOuter = adaptationSet.Elements().Where(e => e.Name.LocalName == "SegmentTemplate");
//representation中的segmentTemplate // representation中的segmentTemplate
var segmentTemplateElements = representation.Elements().Where(e => e.Name.LocalName == "SegmentTemplate"); var segmentTemplateElements = representation.Elements().Where(e => e.Name.LocalName == "SegmentTemplate");
if (segmentTemplateElements.Any() || segmentTemplateElementsOuter.Any()) if (segmentTemplateElements.Any() || segmentTemplateElementsOuter.Any())
{ {
//优先使用最近的元素 // 优先使用最近的元素
var segmentTemplate = (segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault())!; var segmentTemplate = (segmentTemplateElements.FirstOrDefault() ?? segmentTemplateElementsOuter.FirstOrDefault())!;
var segmentTemplateOuter = (segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault())!; var segmentTemplateOuter = (segmentTemplateElementsOuter.FirstOrDefault() ?? segmentTemplateElements.FirstOrDefault())!;
var varDic = new Dictionary<string, object?>(); var varDic = new Dictionary<string, object?>();
varDic[DASHTags.TemplateRepresentationID] = streamSpec.GroupId; varDic[DASHTags.TemplateRepresentationID] = streamSpec.GroupId;
varDic[DASHTags.TemplateBandwidth] = bandwidth?.Value; varDic[DASHTags.TemplateBandwidth] = bandwidth?.Value;
//presentationTimeOffset // presentationTimeOffset
var presentationTimeOffsetStr = segmentTemplate.Attribute("presentationTimeOffset")?.Value ?? segmentTemplateOuter.Attribute("presentationTimeOffset")?.Value ?? "0"; var presentationTimeOffsetStr = segmentTemplate.Attribute("presentationTimeOffset")?.Value ?? segmentTemplateOuter.Attribute("presentationTimeOffset")?.Value ?? "0";
//timesacle // timesacle
var timescaleStr = segmentTemplate.Attribute("timescale")?.Value ?? segmentTemplateOuter.Attribute("timescale")?.Value ?? "1"; var timescaleStr = segmentTemplate.Attribute("timescale")?.Value ?? segmentTemplateOuter.Attribute("timescale")?.Value ?? "1";
var durationStr = segmentTemplate.Attribute("duration")?.Value ?? segmentTemplateOuter.Attribute("duration")?.Value; var durationStr = segmentTemplate.Attribute("duration")?.Value ?? segmentTemplateOuter.Attribute("duration")?.Value;
var startNumberStr = segmentTemplate.Attribute("startNumber")?.Value ?? segmentTemplateOuter.Attribute("startNumber")?.Value ?? "1"; var startNumberStr = segmentTemplate.Attribute("startNumber")?.Value ?? segmentTemplateOuter.Attribute("startNumber")?.Value ?? "1";
//处理init url // 处理init url
var initialization = segmentTemplate.Attribute("initialization")?.Value ?? segmentTemplateOuter.Attribute("initialization")?.Value; var initialization = segmentTemplate.Attribute("initialization")?.Value ?? segmentTemplateOuter.Attribute("initialization")?.Value;
if (initialization != null) if (initialization != null)
{ {
var _init = ParserUtil.ReplaceVars(initialization, varDic); var _init = ParserUtil.ReplaceVars(initialization, varDic);
var initUrl = ParserUtil.CombineURL(segBaseUrl, _init); var initUrl = ParserUtil.CombineURL(segBaseUrl, _init);
streamSpec.Playlist.MediaInit = new MediaSegment(); streamSpec.Playlist.MediaInit = new MediaSegment();
streamSpec.Playlist.MediaInit.Index = -1; //便于排序 streamSpec.Playlist.MediaInit.Index = -1; // 便于排序
streamSpec.Playlist.MediaInit.Url = initUrl; streamSpec.Playlist.MediaInit.Url = initUrl;
} }
//处理分片 // 处理分片
var mediaTemplate = segmentTemplate.Attribute("media")?.Value ?? segmentTemplateOuter.Attribute("media")?.Value; var mediaTemplate = segmentTemplate.Attribute("media")?.Value ?? segmentTemplateOuter.Attribute("media")?.Value;
var segmentTimeline = segmentTemplate.Elements().Where(e => e.Name.LocalName == "SegmentTimeline").FirstOrDefault(); var segmentTimeline = segmentTemplate.Elements().FirstOrDefault(e => e.Name.LocalName == "SegmentTimeline");
if (segmentTimeline != null) if (segmentTimeline != null)
{ {
//使用了SegmentTimeline 结果精确 // 使用了SegmentTimeline 结果精确
var segNumber = Convert.ToInt64(startNumberStr); var segNumber = Convert.ToInt64(startNumberStr);
var Ss = segmentTimeline.Elements().Where(e => e.Name.LocalName == "S"); var Ss = segmentTimeline.Elements().Where(e => e.Name.LocalName == "S");
var currentTime = 0L; var currentTime = 0L;
var segIndex = 0; var segIndex = 0;
foreach (var S in Ss) foreach (var S in Ss)
{ {
//每个S元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration) // 每个S元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration)
var _startTimeStr = S.Attribute("t")?.Value; var _startTimeStr = S.Attribute("t")?.Value;
var _durationStr = S.Attribute("d")?.Value; var _durationStr = S.Attribute("d")?.Value;
var _repeatCountStr = S.Attribute("r")?.Value; var _repeatCountStr = S.Attribute("r")?.Value;
@ -371,7 +376,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
if (_repeatCount < 0) if (_repeatCount < 0)
{ {
//负数表示一直重复 直到period结束 注意减掉已经加入的1个片段 // 负数表示一直重复 直到period结束 注意减掉已经加入的1个片段
_repeatCount = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / _duration) - 1; _repeatCount = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / _duration) - 1;
} }
for (long i = 0; i < _repeatCount; i++) for (long i = 0; i < _repeatCount; i++)
@ -395,22 +400,22 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
else else
{ {
//没用SegmentTimeline 需要计算总分片数量 不精确 // 没用SegmentTimeline 需要计算总分片数量 不精确
var timescale = Convert.ToInt32(timescaleStr); var timescale = Convert.ToInt32(timescaleStr);
var startNumber = Convert.ToInt64(startNumberStr); var startNumber = Convert.ToInt64(startNumberStr);
var duration = Convert.ToInt32(durationStr); var duration = Convert.ToInt32(durationStr);
var totalNumber = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / duration); var totalNumber = (long)Math.Ceiling(XmlConvert.ToTimeSpan(periodDuration ?? mediaPresentationDuration ?? "PT0S").TotalSeconds * timescale / duration);
//直播的情况需要自己计算totalNumber // 直播的情况需要自己计算totalNumber
if (totalNumber == 0 && isLive) if (totalNumber == 0 && isLive)
{ {
var now = DateTime.Now; var now = DateTime.Now;
var availableTime = DateTime.Parse(availabilityStartTime!); var availableTime = DateTime.Parse(availabilityStartTime!);
//可用时间+偏移量 // 可用时间+偏移量
var offsetMs = TimeSpan.FromMilliseconds(Convert.ToInt64(presentationTimeOffsetStr) / 1000); var offsetMs = TimeSpan.FromMilliseconds(Convert.ToInt64(presentationTimeOffsetStr) / 1000);
availableTime = availableTime.Add(offsetMs); availableTime = availableTime.Add(offsetMs);
var ts = now - availableTime; var ts = now - availableTime;
var updateTs = XmlConvert.ToTimeSpan(timeShiftBufferDepth!); var updateTs = XmlConvert.ToTimeSpan(timeShiftBufferDepth!);
//(当前时间到发布时间的时间差 - 最小刷新间隔) / 分片时长 // (当前时间到发布时间的时间差 - 最小刷新间隔) / 分片时长
startNumber += (long)((ts.TotalSeconds - updateTs.TotalSeconds) * timescale / duration); startNumber += (long)((ts.TotalSeconds - updateTs.TotalSeconds) * timescale / duration);
totalNumber = (long)(updateTs.TotalSeconds * timescale / duration); totalNumber = (long)(updateTs.TotalSeconds * timescale / duration);
} }
@ -424,14 +429,14 @@ namespace N_m3u8DL_RE.Parser.Extractor
mediaSegment.Url = mediaUrl; mediaSegment.Url = mediaUrl;
if (hasNumber) if (hasNumber)
mediaSegment.NameFromVar = index.ToString(); mediaSegment.NameFromVar = index.ToString();
mediaSegment.Index = isLive ? index : segIndex; //直播直接用startNumber mediaSegment.Index = isLive ? index : segIndex; // 直播直接用startNumber
mediaSegment.Duration = duration / (double)timescale; mediaSegment.Duration = duration / (double)timescale;
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
} }
} }
} }
//如果依旧没被添加分片直接把BaseUrl塞进去就好 // 如果依旧没被添加分片直接把BaseUrl塞进去就好
if (streamSpec.Playlist.MediaParts[0].MediaSegments.Count == 0) if (streamSpec.Playlist.MediaParts[0].MediaSegments.Count == 0)
{ {
streamSpec.Playlist.MediaParts[0].MediaSegments.Add streamSpec.Playlist.MediaParts[0].MediaSegments.Add
@ -445,7 +450,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
); );
} }
//判断加密情况 // 判断加密情况
if (adaptationSet.Elements().Concat(representation.Elements()).Any(e => e.Name.LocalName == "ContentProtection")) if (adaptationSet.Elements().Concat(representation.Elements()).Any(e => e.Name.LocalName == "ContentProtection"))
{ {
if (streamSpec.Playlist.MediaInit != null) if (streamSpec.Playlist.MediaInit != null)
@ -458,17 +463,17 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
} }
//处理同一ID分散在不同Period的情况 // 处理同一ID分散在不同Period的情况
var _index = streamList.FindIndex(_f => _f.PeriodId != streamSpec.PeriodId && _f.GroupId == streamSpec.GroupId && _f.Resolution == streamSpec.Resolution && _f.MediaType == streamSpec.MediaType); var _index = streamList.FindIndex(_f => _f.PeriodId != streamSpec.PeriodId && _f.GroupId == streamSpec.GroupId && _f.Resolution == streamSpec.Resolution && _f.MediaType == streamSpec.MediaType);
if (_index > -1) if (_index > -1)
{ {
if (isLive) if (isLive)
{ {
//直播,这种情况直接略过新的 // 直播,这种情况直接略过新的
} }
else else
{ {
//点播这种情况如果URL不同则作为新的part出现否则仅把时间加起来 // 点播这种情况如果URL不同则作为新的part出现否则仅把时间加起来
var url1 = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Url; var url1 = streamList[_index].Playlist!.MediaParts.Last().MediaSegments.Last().Url;
var url2 = streamSpec.Playlist.MediaParts[0].MediaSegments.LastOrDefault()?.Url; var url2 = streamSpec.Playlist.MediaParts[0].MediaSegments.LastOrDefault()?.Url;
if (url1 != url2) if (url1 != url2)
@ -492,45 +497,42 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
else else
{ {
//修复mp4类型字幕 // 修复mp4类型字幕
if (streamSpec.MediaType == MediaType.SUBTITLES && streamSpec.Extension == "mp4") if (streamSpec is { MediaType: MediaType.SUBTITLES, Extension: "mp4" })
{ {
streamSpec.Extension = "m4s"; streamSpec.Extension = "m4s";
} }
//分片默认后缀m4s // 分片默认后缀m4s
if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == null || streamSpec.Playlist.MediaParts.Sum(x => x.MediaSegments.Count) > 1)) if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == null || streamSpec.Playlist.MediaParts.Sum(x => x.MediaSegments.Count) > 1))
{ {
streamSpec.Extension = "m4s"; streamSpec.Extension = "m4s";
} }
streamList.Add(streamSpec); streamList.Add(streamSpec);
} }
//恢复BaseURL相对位置 // 恢复BaseURL相对位置
segBaseUrl = representationsBaseUrl; segBaseUrl = representationsBaseUrl;
} }
//恢复BaseURL相对位置 // 恢复BaseURL相对位置
segBaseUrl = adaptationSetsBaseUrl; segBaseUrl = adaptationSetsBaseUrl;
} }
} }
//为视频设置默认轨道 // 为视频设置默认轨道
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO); var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES); var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();
foreach (var item in streamList) foreach (var item in streamList.Where(item => !string.IsNullOrEmpty(item.Resolution)))
{ {
if (!string.IsNullOrEmpty(item.Resolution)) if (aL.Count != 0)
{
if (aL.Any())
{ {
item.AudioId = aL.OrderByDescending(x => x.Bandwidth).First().GroupId; item.AudioId = aL.OrderByDescending(x => x.Bandwidth).First().GroupId;
} }
if (sL.Any()) if (sL.Count != 0)
{ {
item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId; item.SubtitleId = sL.OrderByDescending(x => x.Bandwidth).First().GroupId;
} }
} }
}
return streamList; return Task.FromResult(streamList);
} }
/// <summary> /// <summary>
@ -541,8 +543,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
private string? FilterLanguage(string? v) private string? FilterLanguage(string? v)
{ {
if (v == null) return null; if (v == null) return null;
if (Regex.IsMatch(v, "^[\\w_\\-\\d]+$")) return v; return LangCodeRegex().IsMatch(v) ? v : "und";
return "und";
} }
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs) public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
@ -556,7 +557,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
catch (HttpRequestException) when (ParserConfig.Url!= ParserConfig.OriginalUrl) catch (HttpRequestException) when (ParserConfig.Url!= ParserConfig.OriginalUrl)
{ {
//当URL无法访问时再请求原始URL // 当URL无法访问时再请求原始URL
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers); (rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);
} }
@ -566,45 +567,46 @@ namespace N_m3u8DL_RE.Parser.Extractor
var newStreams = await ExtractStreamsAsync(rawText); var newStreams = await ExtractStreamsAsync(rawText);
foreach (var streamSpec in streamSpecs) foreach (var streamSpec in streamSpecs)
{ {
//有的网站每次请求MPD返回的码率不一致导致ToShortString()无法匹配 无法更新playlist // 有的网站每次请求MPD返回的码率不一致导致ToShortString()无法匹配 无法更新playlist
//故增加通过init url来匹配 (如果有的话) // 故增加通过init url来匹配 (如果有的话)
var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString()); var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());
if (!match.Any()) if (!match.Any())
match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url); match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);
if (match.Any()) if (match.Any())
streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; //不更新init streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; // 不更新init
} }
//这里才调用URL预处理器节省开销 // 这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs); await ProcessUrlAsync(streamSpecs);
} }
private async Task ProcessUrlAsync(List<StreamSpec> streamSpecs) private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
{ {
for (int i = 0; i < streamSpecs.Count; i++) foreach (var streamSpec in streamSpecs)
{
var playlist = streamSpecs[i].Playlist;
if (playlist != null)
{ {
var playlist = streamSpec.Playlist;
if (playlist == null) continue;
if (playlist.MediaInit != null) if (playlist.MediaInit != null)
{ {
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url); playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
} }
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++) for (var ii = 0; ii < playlist!.MediaParts.Count; ii++)
{ {
var part = playlist.MediaParts[ii]; var part = playlist.MediaParts[ii];
for (int iii = 0; iii < part.MediaSegments.Count; iii++) foreach (var mediaSegment in part.MediaSegments)
{ {
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url); mediaSegment.Url = PreProcessUrl(mediaSegment.Url);
}
}
} }
} }
} }
return Task.CompletedTask;
}
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs) public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{ {
//这里才调用URL预处理器节省开销 // 这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs); await ProcessUrlAsync(streamSpecs);
} }
@ -631,5 +633,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
} }
} }
}
[GeneratedRegex(@"^[\w_\-\d]+$")]
private static partial Regex LangCodeRegex();
} }

View File

@ -5,18 +5,12 @@ using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Util;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
internal class HLSExtractor : IExtractor
{ {
internal class HLSExtractor : IExtractor
{
public ExtractorType ExtractorType => ExtractorType.HLS; public ExtractorType ExtractorType => ExtractorType.HLS;
private string M3u8Url = string.Empty; private string M3u8Url = string.Empty;
@ -26,8 +20,6 @@ namespace N_m3u8DL_RE.Parser.Extractor
public ParserConfig ParserConfig { get; set; } public ParserConfig ParserConfig { get; set; }
private HLSExtractor() { }
public HLSExtractor(ParserConfig parserConfig) public HLSExtractor(ParserConfig parserConfig)
{ {
this.ParserConfig = parserConfig; this.ParserConfig = parserConfig;
@ -37,14 +29,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
private void SetBaseUrl() private void SetBaseUrl()
{ {
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.M3u8Url;
{
this.BaseUrl = ParserConfig.BaseUrl;
}
else
{
this.BaseUrl = this.M3u8Url;
}
} }
/// <summary> /// <summary>
@ -83,11 +68,11 @@ namespace N_m3u8DL_RE.Parser.Extractor
return url; return url;
} }
private async Task<List<StreamSpec>> ParseMasterListAsync() private Task<List<StreamSpec>> ParseMasterListAsync()
{ {
MasterM3u8Flag = true; MasterM3u8Flag = true;
List<StreamSpec> streams = new List<StreamSpec>(); List<StreamSpec> streams = [];
using StringReader sr = new StringReader(M3u8Content); using StringReader sr = new StringReader(M3u8Content);
string? line; string? line;
@ -128,7 +113,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
if (!string.IsNullOrEmpty(videoRange)) if (!string.IsNullOrEmpty(videoRange))
streamSpec.VideoRange = videoRange; streamSpec.VideoRange = videoRange;
//清除多余的编码信息 dvh1.05.06,ec-3 => dvh1.05.06 // 清除多余的编码信息 dvh1.05.06,ec-3 => dvh1.05.06
if (!string.IsNullOrEmpty(streamSpec.Codecs) && !string.IsNullOrEmpty(streamSpec.AudioId)) if (!string.IsNullOrEmpty(streamSpec.Codecs) && !string.IsNullOrEmpty(streamSpec.AudioId))
{ {
streamSpec.Codecs = streamSpec.Codecs.Split(',')[0]; streamSpec.Codecs = streamSpec.Codecs.Split(',')[0];
@ -145,7 +130,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
streamSpec.MediaType = mediaType; streamSpec.MediaType = mediaType;
} }
//跳过CLOSED_CAPTIONS类型目前不支持 // 跳过CLOSED_CAPTIONS类型目前不支持
if (streamSpec.MediaType == MediaType.CLOSED_CAPTIONS) if (streamSpec.MediaType == MediaType.CLOSED_CAPTIONS)
{ {
continue; continue;
@ -201,7 +186,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
streams.Add(streamSpec); streams.Add(streamSpec);
} }
else if (line.StartsWith("#")) else if (line.StartsWith('#'))
{ {
continue; continue;
} }
@ -214,13 +199,19 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
} }
return streams; return Task.FromResult(streams);
} }
private async Task<Playlist> ParseListAsync() private Task<Playlist> ParseListAsync()
{ {
//标记是否已清除优酷广告分片 // 标记是否已清除广告分片
bool hasAd = false; bool hasAd = false;
;
bool allowHlsMultiExtMap = ParserConfig.CustomParserArgs.TryGetValue("AllowHlsMultiExtMap", out var allMultiExtMap) && allMultiExtMap == "true";
if (allowHlsMultiExtMap)
{
Logger.WarnMarkUp($"[darkorange3_1]{ResString.allowHlsMultiExtMap}[/]");
}
using StringReader sr = new StringReader(M3u8Content); using StringReader sr = new StringReader(M3u8Content);
string? line; string? line;
@ -231,22 +222,22 @@ namespace N_m3u8DL_RE.Parser.Extractor
long startIndex; long startIndex;
Playlist playlist = new(); Playlist playlist = new();
List<MediaPart> mediaParts = new(); List<MediaPart> mediaParts = [];
//当前的加密信息 // 当前的加密信息
EncryptInfo currentEncryptInfo = new(); EncryptInfo currentEncryptInfo = new();
if (ParserConfig.CustomMethod != null) if (ParserConfig.CustomMethod != null)
currentEncryptInfo.Method = ParserConfig.CustomMethod.Value; currentEncryptInfo.Method = ParserConfig.CustomMethod.Value;
if (ParserConfig.CustomeKey != null && ParserConfig.CustomeKey.Length > 0) if (ParserConfig.CustomeKey is { Length: > 0 })
currentEncryptInfo.Key = ParserConfig.CustomeKey; currentEncryptInfo.Key = ParserConfig.CustomeKey;
if (ParserConfig.CustomeIV != null && ParserConfig.CustomeIV.Length > 0) if (ParserConfig.CustomeIV is { Length: > 0 })
currentEncryptInfo.IV = ParserConfig.CustomeIV; currentEncryptInfo.IV = ParserConfig.CustomeIV;
//上次读取到的加密行,#EXT-X-KEY:…… // 上次读取到的加密行,#EXT-X-KEY:……
string lastKeyLine = ""; string lastKeyLine = "";
MediaPart mediaPart = new(); MediaPart mediaPart = new();
MediaSegment segment = new(); MediaSegment segment = new();
List<MediaSegment> segments = new(); List<MediaSegment> segments = [];
while ((line = sr.ReadLine()) != null) while ((line = sr.ReadLine()) != null)
@ -254,7 +245,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
if (string.IsNullOrEmpty(line)) if (string.IsNullOrEmpty(line))
continue; continue;
//只下载部分字节 // 只下载部分字节
if (line.StartsWith(HLSTags.ext_x_byterange)) if (line.StartsWith(HLSTags.ext_x_byterange))
{ {
var p = ParserUtil.GetAttribute(line); var p = ParserUtil.GetAttribute(line);
@ -263,7 +254,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
segment.StartRange = o ?? segments.Last().StartRange + segments.Last().ExpectLength; segment.StartRange = o ?? segments.Last().StartRange + segments.Last().ExpectLength;
expectSegment = true; expectSegment = true;
} }
//国家地理去广告 // 国家地理去广告
else if (line.StartsWith("#UPLYNK-SEGMENT")) else if (line.StartsWith("#UPLYNK-SEGMENT"))
{ {
if (line.Contains(",ad")) if (line.Contains(",ad"))
@ -271,58 +262,57 @@ namespace N_m3u8DL_RE.Parser.Extractor
else if (line.Contains(",segment")) else if (line.Contains(",segment"))
isAd = false; isAd = false;
} }
//国家地理去广告 // 国家地理去广告
else if (isAd) else if (isAd)
{ {
continue; continue;
} }
//解析定义的分段长度 // 解析定义的分段长度
else if (line.StartsWith(HLSTags.ext_x_targetduration)) else if (line.StartsWith(HLSTags.ext_x_targetduration))
{ {
playlist.TargetDuration = Convert.ToDouble(ParserUtil.GetAttribute(line)); playlist.TargetDuration = Convert.ToDouble(ParserUtil.GetAttribute(line));
} }
//解析起始编号 // 解析起始编号
else if (line.StartsWith(HLSTags.ext_x_media_sequence)) else if (line.StartsWith(HLSTags.ext_x_media_sequence))
{ {
segIndex = Convert.ToInt64(ParserUtil.GetAttribute(line)); segIndex = Convert.ToInt64(ParserUtil.GetAttribute(line));
startIndex = segIndex; startIndex = segIndex;
} }
//program date time // program date time
else if (line.StartsWith(HLSTags.ext_x_program_date_time)) else if (line.StartsWith(HLSTags.ext_x_program_date_time))
{ {
segment.DateTime = DateTime.Parse(ParserUtil.GetAttribute(line)); segment.DateTime = DateTime.Parse(ParserUtil.GetAttribute(line));
} }
//解析不连续标记需要单独合并timestamp不同 // 解析不连续标记需要单独合并timestamp不同
else if (line.StartsWith(HLSTags.ext_x_discontinuity)) else if (line.StartsWith(HLSTags.ext_x_discontinuity))
{ {
//修复优酷去除广告后的遗留问题 // 修复YK去除广告后的遗留问题
if (hasAd && mediaParts.Count > 0) if (hasAd && mediaParts.Count > 0)
{ {
segments = mediaParts[mediaParts.Count - 1].MediaSegments; segments = mediaParts[^1].MediaSegments;
mediaParts.RemoveAt(mediaParts.Count - 1); mediaParts.RemoveAt(mediaParts.Count - 1);
hasAd = false; hasAd = false;
continue; continue;
} }
//常规情况的#EXT-X-DISCONTINUITY标记新建part // 常规情况的#EXT-X-DISCONTINUITY标记新建part
if (!hasAd && segments.Count >= 1) if (hasAd || segments.Count < 1) continue;
{
mediaParts.Add(new MediaPart() mediaParts.Add(new MediaPart
{ {
MediaSegments = segments, MediaSegments = segments,
}); });
segments = new(); segments = new();
} }
} // 解析KEY
//解析KEY
else if (line.StartsWith(HLSTags.ext_x_key)) else if (line.StartsWith(HLSTags.ext_x_key))
{ {
var uri = ParserUtil.GetAttribute(line, "URI"); var uri = ParserUtil.GetAttribute(line, "URI");
var uri_last = ParserUtil.GetAttribute(lastKeyLine, "URI"); var uri_last = ParserUtil.GetAttribute(lastKeyLine, "URI");
//如果KEY URL相同不进行重复解析 // 如果KEY URL相同不进行重复解析
if (uri != uri_last) if (uri != uri_last)
{ {
//调用处理器进行解析 // 调用处理器进行解析
var parsedInfo = ParseKey(line); var parsedInfo = ParseKey(line);
currentEncryptInfo.Method = parsedInfo.Method; currentEncryptInfo.Method = parsedInfo.Method;
currentEncryptInfo.Key = parsedInfo.Key; currentEncryptInfo.Key = parsedInfo.Key;
@ -330,13 +320,13 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
lastKeyLine = line; lastKeyLine = line;
} }
//解析分片时长 // 解析分片时长
else if (line.StartsWith(HLSTags.extinf)) else if (line.StartsWith(HLSTags.extinf))
{ {
string[] tmp = ParserUtil.GetAttribute(line).Split(','); string[] tmp = ParserUtil.GetAttribute(line).Split(',');
segment.Duration = Convert.ToDouble(tmp[0]); segment.Duration = Convert.ToDouble(tmp[0]);
segment.Index = segIndex; segment.Index = segIndex;
//是否有加密有的话写入KEY和IV // 是否有加密有的话写入KEY和IV
if (currentEncryptInfo.Method != EncryptMethod.NONE) if (currentEncryptInfo.Method != EncryptMethod.NONE)
{ {
segment.EncryptInfo.Method = currentEncryptInfo.Method; segment.EncryptInfo.Method = currentEncryptInfo.Method;
@ -346,7 +336,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
expectSegment = true; expectSegment = true;
segIndex++; segIndex++;
} }
//m3u8主体结束 // m3u8主体结束
else if (line.StartsWith(HLSTags.ext_x_endlist)) else if (line.StartsWith(HLSTags.ext_x_endlist))
{ {
if (segments.Count > 0) if (segments.Count > 0)
@ -359,15 +349,15 @@ namespace N_m3u8DL_RE.Parser.Extractor
segments = new(); segments = new();
isEndlist = true; isEndlist = true;
} }
//#EXT-X-MAP // #EXT-X-MAP
else if (line.StartsWith(HLSTags.ext_x_map)) else if (line.StartsWith(HLSTags.ext_x_map))
{ {
if (playlist.MediaInit == null) if (playlist.MediaInit == null || hasAd)
{ {
playlist.MediaInit = new MediaSegment() playlist.MediaInit = new MediaSegment()
{ {
Url = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, ParserUtil.GetAttribute(line, "URI"))), Url = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, ParserUtil.GetAttribute(line, "URI"))),
Index = -1, //便于排序 Index = -1, // 便于排序
}; };
if (line.Contains("BYTERANGE")) if (line.Contains("BYTERANGE"))
{ {
@ -376,15 +366,13 @@ namespace N_m3u8DL_RE.Parser.Extractor
playlist.MediaInit.ExpectLength = n; playlist.MediaInit.ExpectLength = n;
playlist.MediaInit.StartRange = o ?? 0L; playlist.MediaInit.StartRange = o ?? 0L;
} }
//是否有加密有的话写入KEY和IV if (currentEncryptInfo.Method == EncryptMethod.NONE) continue;
if (currentEncryptInfo.Method != EncryptMethod.NONE) // 有加密的话写入KEY和IV
{
playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method; playlist.MediaInit.EncryptInfo.Method = currentEncryptInfo.Method;
playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key; playlist.MediaInit.EncryptInfo.Key = currentEncryptInfo.Key;
playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0')); playlist.MediaInit.EncryptInfo.IV = currentEncryptInfo.IV ?? HexUtil.HexToBytes(Convert.ToString(segIndex, 16).PadLeft(32, '0'));
} }
} // 遇到了其他的map说明已经不是一个视频了全部丢弃即可
//遇到了其他的map说明已经不是一个视频了全部丢弃即可
else else
{ {
if (segments.Count > 0) if (segments.Count > 0)
@ -395,31 +383,34 @@ namespace N_m3u8DL_RE.Parser.Extractor
}); });
} }
segments = new(); segments = new();
if (!allowHlsMultiExtMap)
{
isEndlist = true; isEndlist = true;
break; break;
} }
} }
//评论行不解析 }
else if (line.StartsWith("#")) continue; // 评论行不解析
//空白行不解析 else if (line.StartsWith('#')) continue;
// 空白行不解析
else if (line.StartsWith("\r\n")) continue; else if (line.StartsWith("\r\n")) continue;
//解析分片的地址 // 解析分片的地址
else if (expectSegment) else if (expectSegment)
{ {
var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, line)); var segUrl = PreProcessUrl(ParserUtil.CombineURL(BaseUrl, line));
segment.Url = segUrl; segment.Url = segUrl;
segments.Add(segment); segments.Add(segment);
segment = new(); segment = new();
//优酷的广告分段则清除此分片 // YK的广告分段则清除此分片
//需要注意,遇到广告说明程序对上文的#EXT-X-DISCONTINUITY做出的动作是不必要的 // 需要注意,遇到广告说明程序对上文的#EXT-X-DISCONTINUITY做出的动作是不必要的
//其实上下文是同一种编码需要恢复到原先的part上 // 其实上下文是同一种编码需要恢复到原先的part上
if (segUrl.Contains("ccode=") && segUrl.Contains("/ad/") && segUrl.Contains("duration=")) if (segUrl.Contains("ccode=") && segUrl.Contains("/ad/") && segUrl.Contains("duration="))
{ {
segments.RemoveAt(segments.Count - 1); segments.RemoveAt(segments.Count - 1);
segIndex--; segIndex--;
hasAd = true; hasAd = true;
} }
//优酷广告(4K分辨率测试) // YK广告(4K分辨率测试)
if (segUrl.Contains("ccode=0902") && segUrl.Contains("duration=")) if (segUrl.Contains("ccode=0902") && segUrl.Contains("duration="))
{ {
segments.RemoveAt(segments.Count - 1); segments.RemoveAt(segments.Count - 1);
@ -430,7 +421,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
} }
//直播的情况无法遇到m3u8结束标记需要手动将segments加入parts // 直播的情况无法遇到m3u8结束标记需要手动将segments加入parts
if (!isEndlist) if (!isEndlist)
{ {
mediaParts.Add(new MediaPart() mediaParts.Add(new MediaPart()
@ -442,14 +433,14 @@ namespace N_m3u8DL_RE.Parser.Extractor
playlist.MediaParts = mediaParts; playlist.MediaParts = mediaParts;
playlist.IsLive = !isEndlist; playlist.IsLive = !isEndlist;
//直播刷新间隔 // 直播刷新间隔
if (playlist.IsLive) if (playlist.IsLive)
{ {
//由于播放器默认从最后3个分片开始播放 此处设置刷新间隔为TargetDuration的2倍 // 由于播放器默认从最后3个分片开始播放 此处设置刷新间隔为TargetDuration的2倍
playlist.RefreshIntervalMs = (int)((playlist.TargetDuration ?? 5) * 2 * 1000); playlist.RefreshIntervalMs = (int)((playlist.TargetDuration ?? 5) * 2 * 1000);
} }
return playlist; return Task.FromResult(playlist);
} }
private EncryptInfo ParseKey(string keyLine) private EncryptInfo ParseKey(string keyLine)
@ -458,7 +449,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
{ {
if (p.CanProcess(ExtractorType, keyLine, M3u8Url, M3u8Content, ParserConfig)) if (p.CanProcess(ExtractorType, keyLine, M3u8Url, M3u8Content, ParserConfig))
{ {
//匹配到对应处理器后不再继续 // 匹配到对应处理器后不再继续
return p.Process(keyLine, M3u8Url, M3u8Content, ParserConfig); return p.Process(keyLine, M3u8Url, M3u8Content, ParserConfig);
} }
} }
@ -477,24 +468,22 @@ namespace N_m3u8DL_RE.Parser.Extractor
lists = lists.DistinctBy(p => p.Url).ToList(); lists = lists.DistinctBy(p => p.Url).ToList();
return lists; return lists;
} }
else
{
var playlist = await ParseListAsync(); var playlist = await ParseListAsync();
return new List<StreamSpec>() return
{ [
new StreamSpec() new()
{ {
Url = ParserConfig.Url, Url = ParserConfig.Url,
Playlist = playlist, Playlist = playlist,
Extension = playlist.MediaInit != null ? "mp4" : "ts" Extension = playlist.MediaInit != null ? "mp4" : "ts"
} }
}; ];
}
} }
private async Task LoadM3u8FromUrlAsync(string url) private async Task LoadM3u8FromUrlAsync(string url)
{ {
//Logger.Info(ResString.loadingUrl + url); // Logger.Info(ResString.loadingUrl + url);
if (url.StartsWith("file:")) if (url.StartsWith("file:"))
{ {
var uri = new Uri(url); var uri = new Uri(url);
@ -508,7 +497,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
catch (HttpRequestException) when (url != ParserConfig.OriginalUrl) catch (HttpRequestException) when (url != ParserConfig.OriginalUrl)
{ {
//当URL无法访问时再请求原始URL // 当URL无法访问时再请求原始URL
(this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers); (this.M3u8Content, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);
} }
} }
@ -525,20 +514,19 @@ namespace N_m3u8DL_RE.Parser.Extractor
/// <returns></returns> /// <returns></returns>
private async Task RefreshUrlFromMaster(List<StreamSpec> lists) private async Task RefreshUrlFromMaster(List<StreamSpec> lists)
{ {
//重新加载master m3u8, 刷新选中流的URL // 重新加载master m3u8, 刷新选中流的URL
await LoadM3u8FromUrlAsync(ParserConfig.Url); await LoadM3u8FromUrlAsync(ParserConfig.Url);
var newStreams = await ParseMasterListAsync(); var newStreams = await ParseMasterListAsync();
newStreams = newStreams.DistinctBy(p => p.Url).ToList(); newStreams = newStreams.DistinctBy(p => p.Url).ToList();
foreach (var l in lists) foreach (var l in lists)
{ {
var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()); var match = newStreams.Where(n => n.ToShortString() == l.ToShortString()).ToList();
if (match.Any()) if (match.Count == 0) continue;
{
Logger.DebugMarkUp($"{l.Url} => {match.First().Url}"); Logger.DebugMarkUp($"{l.Url} => {match.First().Url}");
l.Url = match.First().Url; l.Url = match.First().Url;
} }
} }
}
public async Task FetchPlayListAsync(List<StreamSpec> lists) public async Task FetchPlayListAsync(List<StreamSpec> lists)
{ {
@ -546,20 +534,20 @@ namespace N_m3u8DL_RE.Parser.Extractor
{ {
try try
{ {
//直接重新加载m3u8 // 直接重新加载m3u8
await LoadM3u8FromUrlAsync(lists[i].Url!); await LoadM3u8FromUrlAsync(lists[i].Url!);
} }
catch (HttpRequestException) when (MasterM3u8Flag == true) catch (HttpRequestException) when (MasterM3u8Flag)
{ {
Logger.WarnMarkUp("Can not load m3u8. Try refreshing url from master url..."); Logger.WarnMarkUp("Can not load m3u8. Try refreshing url from master url...");
//当前URL无法加载 尝试从Master链接中刷新URL // 当前URL无法加载 尝试从Master链接中刷新URL
await RefreshUrlFromMaster(lists); await RefreshUrlFromMaster(lists);
await LoadM3u8FromUrlAsync(lists[i].Url!); await LoadM3u8FromUrlAsync(lists[i].Url!);
} }
var newPlaylist = await ParseListAsync(); var newPlaylist = await ParseListAsync();
if (lists[i].Playlist?.MediaInit != null) if (lists[i].Playlist?.MediaInit != null)
lists[i].Playlist!.MediaParts = newPlaylist.MediaParts; //不更新init lists[i].Playlist!.MediaParts = newPlaylist.MediaParts; // 不更新init
else else
lists[i].Playlist = newPlaylist; lists[i].Playlist = newPlaylist;
@ -581,5 +569,4 @@ namespace N_m3u8DL_RE.Parser.Extractor
{ {
await FetchPlayListAsync(streamSpecs); await FetchPlayListAsync(streamSpecs);
} }
}
} }

View File

@ -1,16 +1,11 @@
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
public interface IExtractor
{ {
public interface IExtractor
{
ExtractorType ExtractorType { get; } ExtractorType ExtractorType { get; }
ParserConfig ParserConfig { get; set; } ParserConfig ParserConfig { get; set; }
@ -23,5 +18,4 @@ namespace N_m3u8DL_RE.Parser.Extractor
string PreProcessUrl(string url); string PreProcessUrl(string url);
void PreProcessContent(); void PreProcessContent();
}
} }

View File

@ -3,10 +3,10 @@ using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
internal class LiveTSExtractor : IExtractor
{ {
internal class LiveTSExtractor : IExtractor
{
public ExtractorType ExtractorType => ExtractorType.HTTP_LIVE; public ExtractorType ExtractorType => ExtractorType.HTTP_LIVE;
public ParserConfig ParserConfig {get; set;} public ParserConfig ParserConfig {get; set;}
@ -16,26 +16,26 @@ namespace N_m3u8DL_RE.Parser.Extractor
this.ParserConfig = parserConfig; this.ParserConfig = parserConfig;
} }
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText) public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{ {
return new List<StreamSpec>() return Task.FromResult(new List<StreamSpec>
{ {
new StreamSpec() new()
{ {
OriginalUrl = ParserConfig.OriginalUrl, OriginalUrl = ParserConfig.OriginalUrl,
Url = ParserConfig.Url, Url = ParserConfig.Url,
Playlist = new Playlist(), Playlist = new Playlist(),
GroupId = ResString.ReLiveTs GroupId = ResString.ReLiveTs
} }
}; });
} }
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs) public Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public async void PreProcessContent() public void PreProcessContent()
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -49,5 +49,4 @@ namespace N_m3u8DL_RE.Parser.Extractor
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
}
} }

View File

@ -6,23 +6,16 @@ using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Mp4; using N_m3u8DL_RE.Parser.Mp4;
using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Util;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq; using System.Xml.Linq;
namespace N_m3u8DL_RE.Parser.Extractor namespace N_m3u8DL_RE.Parser.Extractor;
// Microsoft Smooth Streaming
// https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/manifest
// file:///C:/Users/nilaoda/Downloads/[MS-SSTR]-180316.pdf
internal partial class MSSExtractor : IExtractor
{ {
//Microsoft Smooth Streaming
//https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/manifest
//file:///C:/Users/nilaoda/Downloads/[MS-SSTR]-180316.pdf
internal partial class MSSExtractor : IExtractor
{
[GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")] [GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")]
private static partial Regex VCodecsRegex(); private static partial Regex VCodecsRegex();
@ -46,13 +39,10 @@ namespace N_m3u8DL_RE.Parser.Extractor
private void SetInitUrl() private void SetInitUrl()
{ {
this.IsmUrl = ParserConfig.Url ?? string.Empty; this.IsmUrl = ParserConfig.Url ?? string.Empty;
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl)) this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.IsmUrl;
this.BaseUrl = ParserConfig.BaseUrl;
else
this.BaseUrl = this.IsmUrl;
} }
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText) public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
{ {
var streamList = new List<StreamSpec>(); var streamList = new List<StreamSpec>();
this.IsmContent = rawText; this.IsmContent = rawText;
@ -60,7 +50,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var xmlDocument = XDocument.Parse(IsmContent); var xmlDocument = XDocument.Parse(IsmContent);
//选中第一个SmoothStreamingMedia节点 // 选中第一个SmoothStreamingMedia节点
var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia"); var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia");
var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000"; var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000";
var durationStr = ssmElement.Attribute("Duration")?.Value; var durationStr = ssmElement.Attribute("Duration")?.Value;
@ -72,7 +62,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var protectionSystemId = ""; var protectionSystemId = "";
var protectionData = ""; var protectionData = "";
//加密检测 // 加密检测
var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Protection"); var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Protection");
if (protectElement != null) if (protectElement != null)
{ {
@ -85,26 +75,26 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
} }
//所有StreamIndex节点 // 所有StreamIndex节点
var streamIndexElements = ssmElement.Elements().Where(e => e.Name.LocalName == "StreamIndex"); var streamIndexElements = ssmElement.Elements().Where(e => e.Name.LocalName == "StreamIndex");
foreach (var streamIndex in streamIndexElements) foreach (var streamIndex in streamIndexElements)
{ {
var type = streamIndex.Attribute("Type")?.Value; //"video" / "audio" / "text" var type = streamIndex.Attribute("Type")?.Value; // "video" / "audio" / "text"
var name = streamIndex.Attribute("Name")?.Value; var name = streamIndex.Attribute("Name")?.Value;
var subType = streamIndex.Attribute("Subtype")?.Value; //text track var subType = streamIndex.Attribute("Subtype")?.Value; // text track
//如果有则不从QualityLevel读取 // 如果有则不从QualityLevel读取
//Bitrate = "{bitrate}" / "{Bitrate}" // Bitrate = "{bitrate}" / "{Bitrate}"
//StartTimeSubstitution = "{start time}" / "{start_time}" // StartTimeSubstitution = "{start time}" / "{start_time}"
var urlPattern = streamIndex.Attribute("Url")?.Value; var urlPattern = streamIndex.Attribute("Url")?.Value;
var language = streamIndex.Attribute("Language")?.Value; var language = streamIndex.Attribute("Language")?.Value;
//去除不规范的语言标签 // 去除不规范的语言标签
if (language?.Length != 3) language = null; if (language?.Length != 3) language = null;
//所有c节点 // 所有c节点
var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == "c"); var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == "c");
//所有QualityLevel节点 // 所有QualityLevel节点
var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == "QualityLevel"); var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == "QualityLevel");
foreach (var qualityLevel in qualityLevelElements) foreach (var qualityLevel in qualityLevelElements)
@ -124,7 +114,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
var channels = qualityLevel.Attribute("Channels")?.Value; var channels = qualityLevel.Attribute("Channels")?.Value;
StreamSpec streamSpec = new(); StreamSpec streamSpec = new();
streamSpec.PublishTime = DateTime.Now; //发布时间默认现在 streamSpec.PublishTime = DateTime.Now; // 发布时间默认现在
streamSpec.Extension = "m4s"; streamSpec.Extension = "m4s";
streamSpec.OriginalUrl = ParserConfig.OriginalUrl; streamSpec.OriginalUrl = ParserConfig.OriginalUrl;
streamSpec.PeriodId = indexStr; streamSpec.PeriodId = indexStr;
@ -148,7 +138,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
streamSpec.Playlist.MediaInit = new MediaSegment(); streamSpec.Playlist.MediaInit = new MediaSegment();
if (!string.IsNullOrEmpty(codecPrivateData)) if (!string.IsNullOrEmpty(codecPrivateData))
{ {
streamSpec.Playlist.MediaInit.Index = -1; //便于排序 streamSpec.Playlist.MediaInit.Index = -1; // 便于排序
streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}"; streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}";
} }
@ -159,7 +149,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
foreach (var c in cElements) foreach (var c in cElements)
{ {
//每个C元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration) // 每个C元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration)
var _startTimeStr = c.Attribute("t")?.Value; var _startTimeStr = c.Attribute("t")?.Value;
var _durationStr = c.Attribute("d")?.Value; var _durationStr = c.Attribute("d")?.Value;
var _repeatCountStr = c.Attribute("r")?.Value; var _repeatCountStr = c.Attribute("r")?.Value;
@ -185,7 +175,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment); streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
if (_repeatCount < 0) if (_repeatCount < 0)
{ {
//负数表示一直重复 直到period结束 注意减掉已经加入的1个片段 // 负数表示一直重复 直到period结束 注意减掉已经加入的1个片段
_repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1; _repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1;
} }
for (long i = 0; i < _repeatCount; i++) for (long i = 0; i < _repeatCount; i++)
@ -205,7 +195,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
currentTime += _duration; currentTime += _duration;
} }
//生成MOOV数据 // 生成MOOV数据
if (MSSMoovProcessor.CanHandle(fourCC!)) if (MSSMoovProcessor.CanHandle(fourCC!))
{ {
streamSpec.MSSData = new MSSData() streamSpec.MSSData = new MSSData()
@ -224,9 +214,9 @@ namespace N_m3u8DL_RE.Parser.Extractor
ProtectionSystemID = protectionSystemId, ProtectionSystemID = protectionSystemId,
}; };
var processor = new MSSMoovProcessor(streamSpec); var processor = new MSSMoovProcessor(streamSpec);
var header = processor.GenHeader(); //trackId可能不正确 var header = processor.GenHeader(); // trackId可能不正确
streamSpec.Playlist!.MediaInit!.Url = $"base64://{Convert.ToBase64String(header)}"; streamSpec.Playlist!.MediaInit!.Url = $"base64://{Convert.ToBase64String(header)}";
//为音视频写入加密信息 // 为音视频写入加密信息
if (isProtection && type != "text") if (isProtection && type != "text")
{ {
if (streamSpec.Playlist.MediaInit != null) if (streamSpec.Playlist.MediaInit != null)
@ -243,30 +233,26 @@ namespace N_m3u8DL_RE.Parser.Extractor
else else
{ {
Logger.WarnMarkUp($"[green]{fourCC}[/] not supported! Skiped."); Logger.WarnMarkUp($"[green]{fourCC}[/] not supported! Skiped.");
continue;
} }
} }
} }
//为视频设置默认轨道 // 为视频设置默认轨道
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO); var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES); var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();
foreach (var item in streamList) foreach (var item in streamList.Where(item => !string.IsNullOrEmpty(item.Resolution)))
{ {
if (!string.IsNullOrEmpty(item.Resolution)) if (aL.Count != 0)
{
if (aL.Any())
{ {
item.AudioId = aL.First().GroupId; item.AudioId = aL.First().GroupId;
} }
if (sL.Any()) if (sL.Count != 0)
{ {
item.SubtitleId = sL.First().GroupId; item.SubtitleId = sL.First().GroupId;
} }
} }
}
return streamList; return Task.FromResult(streamList);
} }
/// <summary> /// <summary>
@ -281,11 +267,11 @@ namespace N_m3u8DL_RE.Parser.Extractor
return fourCC switch return fourCC switch
{ {
//AVC视频 // AVC视频
"H264" or "X264" or "DAVC" or "AVC1" => ParseAVCCodecs(privateData), "H264" or "X264" or "DAVC" or "AVC1" => ParseAVCCodecs(privateData),
//AAC音频 // AAC音频
"AAC" or "AACL" or "AACH" or "AACP" => ParseAACCodecs(fourCC, privateData), "AAC" or "AACL" or "AACH" or "AACP" => ParseAACCodecs(fourCC, privateData),
//默认返回fourCC本身 // 默认返回fourCC本身
_ => fourCC.ToLower() _ => fourCC.ToLower()
}; };
} }
@ -313,31 +299,32 @@ namespace N_m3u8DL_RE.Parser.Extractor
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs) public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
{ {
//这里才调用URL预处理器节省开销 // 这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs); await ProcessUrlAsync(streamSpecs);
} }
private async Task ProcessUrlAsync(List<StreamSpec> streamSpecs) private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
{ {
for (int i = 0; i < streamSpecs.Count; i++) foreach (var streamSpec in streamSpecs)
{
var playlist = streamSpecs[i].Playlist;
if (playlist != null)
{ {
var playlist = streamSpec.Playlist;
if (playlist == null) continue;
if (playlist.MediaInit != null) if (playlist.MediaInit != null)
{ {
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url); playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
} }
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++) for (var ii = 0; ii < playlist!.MediaParts.Count; ii++)
{ {
var part = playlist.MediaParts[ii]; var part = playlist.MediaParts[ii];
for (int iii = 0; iii < part.MediaSegments.Count; iii++) foreach (var segment in part.MediaSegments)
{ {
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url); segment.Url = PreProcessUrl(segment.Url);
}
} }
} }
} }
return Task.CompletedTask;
} }
public string PreProcessUrl(string url) public string PreProcessUrl(string url)
@ -375,7 +362,7 @@ namespace N_m3u8DL_RE.Parser.Extractor
} }
catch (HttpRequestException) when (ParserConfig.Url != ParserConfig.OriginalUrl) catch (HttpRequestException) when (ParserConfig.Url != ParserConfig.OriginalUrl)
{ {
//当URL无法访问时再请求原始URL // 当URL无法访问时再请求原始URL
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers); (rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);
} }
@ -385,17 +372,16 @@ namespace N_m3u8DL_RE.Parser.Extractor
var newStreams = await ExtractStreamsAsync(rawText); var newStreams = await ExtractStreamsAsync(rawText);
foreach (var streamSpec in streamSpecs) foreach (var streamSpec in streamSpecs)
{ {
//有的网站每次请求MPD返回的码率不一致导致ToShortString()无法匹配 无法更新playlist // 有的网站每次请求MPD返回的码率不一致导致ToShortString()无法匹配 无法更新playlist
//故增加通过init url来匹配 (如果有的话) // 故增加通过init url来匹配 (如果有的话)
var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString()); var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());
if (!match.Any()) if (!match.Any())
match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url); match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);
if (match.Any()) if (match.Any())
streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; //不更新init streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; // 不更新init
} }
//这里才调用URL预处理器节省开销 // 这里才调用URL预处理器节省开销
await ProcessUrlAsync(streamSpecs); await ProcessUrlAsync(streamSpecs);
} }
}
} }

View File

@ -1,15 +1,8 @@
using System; namespace Mp4SubtitleParser;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mp4SubtitleParser // make BinaryReader in Big Endian
class BinaryReader2 : BinaryReader
{ {
//make BinaryReader in Big Endian
class BinaryReader2 : BinaryReader
{
public BinaryReader2(System.IO.Stream stream) : base(stream) { } public BinaryReader2(System.IO.Stream stream) : base(stream) { }
public bool HasMoreData() public bool HasMoreData()
@ -66,5 +59,4 @@ namespace Mp4SubtitleParser
Array.Reverse(data); Array.Reverse(data);
return BitConverter.ToUInt64(data, 0); return BitConverter.ToUInt64(data, 0);
} }
}
} }

View File

@ -1,15 +1,10 @@
using System; using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser;
// make BinaryWriter in Big Endian
class BinaryWriter2 : BinaryWriter
{ {
//make BinaryWriter in Big Endian
class BinaryWriter2 : BinaryWriter
{
private static bool IsLittleEndian = BitConverter.IsLittleEndian; private static bool IsLittleEndian = BitConverter.IsLittleEndian;
public BinaryWriter2(System.IO.Stream stream) : base(stream) { } public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
@ -85,5 +80,4 @@ namespace Mp4SubtitleParser
} }
BaseStream.Write(arr); BaseStream.Write(arr);
} }
}
} }

View File

@ -1,5 +1,4 @@
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using System.Security.Cryptography;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser
{ {
@ -11,16 +10,16 @@ namespace Mp4SubtitleParser
public bool isMultiDRM; public bool isMultiDRM;
} }
public class MP4InitUtil public static class MP4InitUtil
{ {
private static readonly byte[] SYSTEM_ID_WIDEVINE = { 0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED }; private static readonly byte[] SYSTEM_ID_WIDEVINE = [0xED, 0xEF, 0x8B, 0xA9, 0x79, 0xD6, 0x4A, 0xCE, 0xA3, 0xC8, 0x27, 0xDC, 0xD5, 0x1D, 0x21, 0xED];
private static readonly byte[] SYSTEM_ID_PLAYREADY = { 0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95 }; private static readonly byte[] SYSTEM_ID_PLAYREADY = [0x9A, 0x04, 0xF0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xAB, 0x92, 0xE6, 0x5B, 0xE0, 0x88, 0x5F, 0x95];
public static ParsedMP4Info ReadInit(byte[] data) public static ParsedMP4Info ReadInit(byte[] data)
{ {
var info = new ParsedMP4Info(); var info = new ParsedMP4Info();
//parse init // parse init
new MP4Parser() new MP4Parser()
.Box("moov", MP4Parser.Children) .Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children) .Box("trak", MP4Parser.Children)
@ -28,22 +27,20 @@ namespace Mp4SubtitleParser
.Box("minf", MP4Parser.Children) .Box("minf", MP4Parser.Children)
.Box("stbl", MP4Parser.Children) .Box("stbl", MP4Parser.Children)
.FullBox("stsd", MP4Parser.SampleDescription) .FullBox("stsd", MP4Parser.SampleDescription)
.FullBox("pssh", (box) => .FullBox("pssh", box =>
{ {
if (!(box.Version == 0 || box.Version == 1)) if (box.Version is not (0 or 1))
throw new Exception("PSSH version can only be 0 or 1"); throw new Exception("PSSH version can only be 0 or 1");
var systemId = box.Reader.ReadBytes(16); var systemId = box.Reader.ReadBytes(16);
if (SYSTEM_ID_WIDEVINE.SequenceEqual(systemId)) if (!SYSTEM_ID_WIDEVINE.SequenceEqual(systemId)) return;
{
var dataSize = box.Reader.ReadUInt32(); var dataSize = box.Reader.ReadUInt32();
var psshData = box.Reader.ReadBytes((int)dataSize); var psshData = box.Reader.ReadBytes((int)dataSize);
info.PSSH = Convert.ToBase64String(psshData); info.PSSH = Convert.ToBase64String(psshData);
if (info.KID == "00000000000000000000000000000000") if (info.KID != "00000000000000000000000000000000") return;
{
info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower(); info.KID = HexUtil.BytesToHex(psshData[2..18]).ToLower();
info.isMultiDRM = true; info.isMultiDRM = true;
}
}
}) })
.FullBox("encv", MP4Parser.AllData(data => ReadBox(data, info))) .FullBox("encv", MP4Parser.AllData(data => ReadBox(data, info)))
.FullBox("enca", MP4Parser.AllData(data => ReadBox(data, info))) .FullBox("enca", MP4Parser.AllData(data => ReadBox(data, info)))
@ -56,12 +53,12 @@ namespace Mp4SubtitleParser
private static void ReadBox(byte[] data, ParsedMP4Info info) private static void ReadBox(byte[] data, ParsedMP4Info info)
{ {
//find schm // find schm
var schmBytes = new byte[4] { 0x73, 0x63, 0x68, 0x6d }; byte[] schmBytes = [0x73, 0x63, 0x68, 0x6d];
var schmIndex = 0; var schmIndex = 0;
for (int i = 0; i < data.Length - 4; i++) for (var i = 0; i < data.Length - 4; i++)
{ {
if (new byte[4] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(schmBytes)) if (new[] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(schmBytes))
{ {
schmIndex = i; schmIndex = i;
break; break;
@ -72,14 +69,14 @@ namespace Mp4SubtitleParser
info.Scheme = System.Text.Encoding.UTF8.GetString(data[schmIndex..][8..12]); info.Scheme = System.Text.Encoding.UTF8.GetString(data[schmIndex..][8..12]);
} }
//if (info.Scheme != "cenc") return; // if (info.Scheme != "cenc") return;
//find KID // find KID
var tencBytes = new byte[4] { 0x74, 0x65, 0x6E, 0x63 }; byte[] tencBytes = [0x74, 0x65, 0x6E, 0x63];
var tencIndex = -1; var tencIndex = -1;
for (int i = 0; i < data.Length - 4; i++) for (int i = 0; i < data.Length - 4; i++)
{ {
if (new byte[4] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(tencBytes)) if (new[] { data[i], data[i + 1], data[i + 2], data[i + 3] }.SequenceEqual(tencBytes))
{ {
tencIndex = i; tencIndex = i;
break; break;

View File

@ -9,12 +9,12 @@ namespace Mp4SubtitleParser
{ {
class ParsedBox class ParsedBox
{ {
public MP4Parser Parser { get; set; } public required MP4Parser Parser { get; set; }
public bool PartialOkay { get; set; } public bool PartialOkay { get; set; }
public long Start { get; set; } public long Start { get; set; }
public uint Version { get; set; } = 1000; public uint Version { get; set; } = 1000;
public uint Flags { get; set; } = 1000; public uint Flags { get; set; } = 1000;
public BinaryReader2 Reader { get; set; } public required BinaryReader2 Reader { get; set; }
public bool Has64BitSize { get; set; } public bool Has64BitSize { get; set; }
} }
@ -28,7 +28,7 @@ namespace Mp4SubtitleParser
class TRUN class TRUN
{ {
public uint SampleCount { get; set; } public uint SampleCount { get; set; }
public List<Sample> SampleData { get; set; } = new List<Sample>(); public List<Sample> SampleData { get; set; } = [];
} }
class Sample class Sample
@ -55,7 +55,7 @@ namespace Mp4SubtitleParser
public static BoxHandler AllData(DataHandler handler) public static BoxHandler AllData(DataHandler handler)
{ {
return (box) => return box =>
{ {
var all = box.Reader.GetLength() - box.Reader.GetPosition(); var all = box.Reader.GetLength() - box.Reader.GetPosition();
handler(box.Reader.ReadBytes((int)all)); handler(box.Reader.ReadBytes((int)all));
@ -111,7 +111,7 @@ namespace Mp4SubtitleParser
var name = TypeToString(type); var name = TypeToString(type);
var has64BitSize = false; var has64BitSize = false;
//Console.WriteLine($"Parsing MP4 box: {name}"); // Console.WriteLine($"Parsing MP4 box: {name}");
switch (size) switch (size)
{ {
@ -129,8 +129,7 @@ namespace Mp4SubtitleParser
break; break;
} }
BoxHandler boxDefinition = null; this.BoxDefinitions.TryGetValue(type, out BoxHandler? boxDefinition);
this.BoxDefinitions.TryGetValue(type, out boxDefinition);
if (boxDefinition != null) if (boxDefinition != null)
{ {
@ -162,7 +161,7 @@ namespace Mp4SubtitleParser
} }
int payloadSize = (int)(end - reader.GetPosition()); int payloadSize = (int)(end - reader.GetPosition());
var payload = (payloadSize > 0) ? reader.ReadBytes(payloadSize) : new byte[0]; var payload = (payloadSize > 0) ? reader.ReadBytes(payloadSize) : [];
var box = new ParsedBox() var box = new ParsedBox()
{ {
Parser = this, Parser = this,

View File

@ -3,15 +3,15 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml; using System.Xml;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser;
class SubEntity
{ {
class SubEntity public required string Begin { get; set; }
{ public required string End { get; set; }
public string Begin { get; set; } public required string Region { get; set; }
public string End { get; set; } public List<XmlElement> Contents { get; set; } = [];
public string Region { get; set; } public List<string> ContentStrings { get; set; } = [];
public List<XmlElement> Contents { get; set; } = new List<XmlElement>();
public List<string> ContentStrings { get; set; } = new List<string>();
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
@ -26,15 +26,15 @@ namespace Mp4SubtitleParser
{ {
return HashCode.Combine(Begin, End, Region, ContentStrings); return HashCode.Combine(Begin, End, Region, ContentStrings);
} }
} }
public partial class MP4TtmlUtil public static partial class MP4TtmlUtil
{ {
[GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")] [GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")]
private static partial Regex AttrRegex(); private static partial Regex AttrRegex();
[GeneratedRegex("<p.*?>(.+?)<\\/p>")] [GeneratedRegex("<p.*?>(.+?)<\\/p>")]
private static partial Regex LabelFixRegex(); private static partial Regex LabelFixRegex();
[GeneratedRegex("\\<tt[\\s\\S]*?\\<\\/tt\\>")] [GeneratedRegex(@"\<tt[\s\S]*?\<\/tt\>")]
private static partial Regex MultiElementsFixRegex(); private static partial Regex MultiElementsFixRegex();
[GeneratedRegex("\\<smpte:image.*xml:id=\\\"(.*?)\\\".*\\>([\\s\\S]*?)<\\/smpte:image>")] [GeneratedRegex("\\<smpte:image.*xml:id=\\\"(.*?)\\\".*\\>([\\s\\S]*?)<\\/smpte:image>")]
private static partial Regex ImageRegex(); private static partial Regex ImageRegex();
@ -43,7 +43,7 @@ namespace Mp4SubtitleParser
{ {
bool sawSTPP = false; bool sawSTPP = false;
//parse init // parse init
new MP4Parser() new MP4Parser()
.Box("moov", MP4Parser.Children) .Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children) .Box("trak", MP4Parser.Children)
@ -51,7 +51,7 @@ namespace Mp4SubtitleParser
.Box("minf", MP4Parser.Children) .Box("minf", MP4Parser.Children)
.Box("stbl", MP4Parser.Children) .Box("stbl", MP4Parser.Children)
.FullBox("stsd", MP4Parser.SampleDescription) .FullBox("stsd", MP4Parser.SampleDescription)
.Box("stpp", (box) => { .Box("stpp", box => {
sawSTPP = true; sawSTPP = true;
}) })
.Parse(data); .Parse(data);
@ -65,7 +65,7 @@ namespace Mp4SubtitleParser
{ {
var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture); var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index); var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index);
return string.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds); return $"{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}.{ts.Milliseconds:000}";
} }
if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc; if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc;
@ -85,12 +85,12 @@ namespace Mp4SubtitleParser
return xmlSrc; return xmlSrc;
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr); var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
//Parse <p> label // Parse <p> label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!) foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{ {
var _begin = _p.GetAttribute("begin"); var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end"); var _end = _p.GetAttribute("end");
//Handle namespace // Handle namespace
foreach (XmlAttribute attr in _p.Attributes) foreach (XmlAttribute attr in _p.Attributes)
{ {
if (attr.LocalName == "begin") _begin = attr.Value; if (attr.LocalName == "begin") _begin = attr.Value;
@ -98,8 +98,8 @@ namespace Mp4SubtitleParser
} }
_p.SetAttribute("begin", Add(_begin)); _p.SetAttribute("begin", Add(_begin));
_p.SetAttribute("end", Add(_end)); _p.SetAttribute("end", Add(_end));
//Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}"); // Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
//Console.WriteLine($"{_end} {_p.GetAttribute("begin")}"); // Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
} }
return xmlDoc.OuterXml; return xmlDoc.OuterXml;
@ -114,7 +114,7 @@ namespace Mp4SubtitleParser
{ {
sb.Append(item.InnerText.Trim()); sb.Append(item.InnerText.Trim());
} }
else if(item.NodeType == XmlNodeType.Element && item.Name == "br") else if(item is { NodeType: XmlNodeType.Element, Name: "br" })
{ {
sb.AppendLine(); sb.AppendLine();
} }
@ -122,30 +122,29 @@ namespace Mp4SubtitleParser
return sb.ToString(); return sb.ToString();
} }
public static List<string> SplitMultipleRootElements(string xml) private static List<string> SplitMultipleRootElements(string xml)
{ {
if (!MultiElementsFixRegex().IsMatch(xml)) return new List<string>(); return !MultiElementsFixRegex().IsMatch(xml) ? [] : MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList();
return MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList();
} }
public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L) public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L)
{ {
return ExtractFromMp4s(new string[] { item }, segTimeMs, baseTimestamp); return ExtractFromMp4s([item], segTimeMs, baseTimestamp);
} }
public static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L) private static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
{ {
//read ttmls // read ttmls
List<string> xmls = new List<string>(); List<string> xmls = [];
int segIndex = 0; int segIndex = 0;
foreach (var item in items) foreach (var item in items)
{ {
var dataSeg = File.ReadAllBytes(item); var dataSeg = File.ReadAllBytes(item);
var sawMDAT = false; var sawMDAT = false;
//parse media // parse media
new MP4Parser() new MP4Parser()
.Box("mdat", MP4Parser.AllData((data) => .Box("mdat", MP4Parser.AllData(data =>
{ {
sawMDAT = true; sawMDAT = true;
// Join this to any previous payload, in case the mp4 has multiple // Join this to any previous payload, in case the mp4 has multiple
@ -161,10 +160,7 @@ namespace Mp4SubtitleParser
else else
{ {
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data)); var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
foreach (var item in datas) xmls.AddRange(datas);
{
xmls.Add(item);
}
} }
})) }))
.Parse(dataSeg,/* partialOkay= */ false); .Parse(dataSeg,/* partialOkay= */ false);
@ -176,25 +172,18 @@ namespace Mp4SubtitleParser
public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L) public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L)
{ {
return ExtractFromTTMLs(new string[] { item }, segTimeMs, baseTimestamp); return ExtractFromTTMLs([item], segTimeMs, baseTimestamp);
} }
public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L) public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
{ {
//read ttmls // read ttmls
List<string> xmls = new List<string>(); List<string> xmls = [];
int segIndex = 0; int segIndex = 0;
foreach (var item in items) foreach (var item in items)
{ {
var xml = File.ReadAllText(item); var xml = File.ReadAllText(item);
if (segTimeMs != 0) xmls.Add(segTimeMs != 0 ? ShiftTime(xml, segTimeMs, segIndex) : xml);
{
xmls.Add(ShiftTime(xml, segTimeMs, segIndex));
}
else
{
xmls.Add(xml);
}
segIndex++; segIndex++;
} }
@ -203,7 +192,7 @@ namespace Mp4SubtitleParser
private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp) private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp)
{ {
//parsing // parsing
var xmlDoc = new XmlDocument(); var xmlDoc = new XmlDocument();
var finalSubs = new List<SubEntity>(); var finalSubs = new List<SubEntity>();
XmlNode? headNode = null; XmlNode? headNode = null;
@ -215,7 +204,7 @@ namespace Mp4SubtitleParser
var xmlContent = item; var xmlContent = item;
if (!xmlContent.Contains("<tt")) continue; if (!xmlContent.Contains("<tt")) continue;
//fix non-standard xml // fix non-standard xml
var xmlContentFix = xmlContent; var xmlContentFix = xmlContent;
if (regex.IsMatch(xmlContent)) if (regex.IsMatch(xmlContent))
{ {
@ -256,8 +245,8 @@ namespace Mp4SubtitleParser
continue; continue;
//PNG Subs // PNG Subs
var imageDic = new Dictionary<string, string>(); //id, Base64 var imageDic = new Dictionary<string, string>(); // id, Base64
if (ImageRegex().IsMatch(xmlDoc.InnerXml)) if (ImageRegex().IsMatch(xmlDoc.InnerXml))
{ {
foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml)) foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))
@ -266,7 +255,7 @@ namespace Mp4SubtitleParser
} }
} }
//convert <div> to <p> // convert <div> to <p>
if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0) if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0)
{ {
foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!) foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!)
@ -277,14 +266,14 @@ namespace Mp4SubtitleParser
} }
} }
//Parse <p> label // Parse <p> label
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!) foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
{ {
var _begin = _p.GetAttribute("begin"); var _begin = _p.GetAttribute("begin");
var _end = _p.GetAttribute("end"); var _end = _p.GetAttribute("end");
var _region = _p.GetAttribute("region"); var _region = _p.GetAttribute("region");
var _bgImg = _p.GetAttribute("smpte:backgroundImage"); var _bgImg = _p.GetAttribute("smpte:backgroundImage");
//Handle namespace // Handle namespace
foreach (XmlAttribute attr in _p.Attributes) foreach (XmlAttribute attr in _p.Attributes)
{ {
if (attr.LocalName == "begin") _begin = attr.Value; if (attr.LocalName == "begin") _begin = attr.Value;
@ -301,7 +290,7 @@ namespace Mp4SubtitleParser
if (string.IsNullOrEmpty(_bgImg)) if (string.IsNullOrEmpty(_bgImg))
{ {
var _spans = _p.ChildNodes; var _spans = _p.ChildNodes;
//Collect <span> // Collect <span>
foreach (XmlNode _node in _spans) foreach (XmlNode _node in _spans)
{ {
if (_node.NodeType == XmlNodeType.Element) if (_node.NodeType == XmlNodeType.Element)
@ -324,28 +313,27 @@ namespace Mp4SubtitleParser
else else
{ {
var id = _bgImg.Replace("#", ""); var id = _bgImg.Replace("#", "");
if (imageDic.ContainsKey(id)) if (imageDic.TryGetValue(id, out var value))
{ {
var _span = new XmlDocument().CreateElement("span"); var _span = new XmlDocument().CreateElement("span");
_span.InnerText = $"Base64::{imageDic[id]}"; _span.InnerText = $"Base64::{value}";
sub.Contents.Add(_span); sub.Contents.Add(_span);
sub.ContentStrings.Add(_span.OuterXml); sub.ContentStrings.Add(_span.OuterXml);
} }
} }
//Check if one <p> has been splitted // Check if one <p> has been splitted
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings)); var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
//Skip empty lines // Skip empty lines
if (sub.ContentStrings.Count > 0) if (sub.ContentStrings.Count <= 0)
{ continue;
//Extend <p> duration // Extend <p> duration
if (index != -1) if (index != -1)
finalSubs[index].End = sub.End; finalSubs[index].End = sub.End;
else if (!finalSubs.Contains(sub)) else if (!finalSubs.Contains(sub))
finalSubs.Add(sub); finalSubs.Add(sub);
} }
} }
}
var dic = new Dictionary<string, string>(); var dic = new Dictionary<string, string>();
@ -372,7 +360,7 @@ namespace Mp4SubtitleParser
} }
StringBuilder vtt = new StringBuilder(); var vtt = new StringBuilder();
vtt.AppendLine("WEBVTT"); vtt.AppendLine("WEBVTT");
foreach (var item in dic) foreach (var item in dic)
{ {
@ -383,5 +371,4 @@ namespace Mp4SubtitleParser
return WebVttSub.Parse(vtt.ToString(), baseTimestamp); return WebVttSub.Parse(vtt.ToString(), baseTimestamp);
} }
}
} }

View File

@ -1,30 +1,30 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using System.Text; using System.Text;
namespace Mp4SubtitleParser namespace Mp4SubtitleParser;
public static class MP4VttUtil
{ {
public class MP4VttUtil
{
public static (bool, uint) CheckInit(byte[] data) public static (bool, uint) CheckInit(byte[] data)
{ {
uint timescale = 0; uint timescale = 0;
bool sawWVTT = false; bool sawWVTT = false;
//parse init // parse init
new MP4Parser() new MP4Parser()
.Box("moov", MP4Parser.Children) .Box("moov", MP4Parser.Children)
.Box("trak", MP4Parser.Children) .Box("trak", MP4Parser.Children)
.Box("mdia", MP4Parser.Children) .Box("mdia", MP4Parser.Children)
.FullBox("mdhd", (box) => .FullBox("mdhd", box =>
{ {
if (!(box.Version == 0 || box.Version == 1)) if (box.Version is not (0 or 1))
throw new Exception("MDHD version can only be 0 or 1"); throw new Exception("MDHD version can only be 0 or 1");
timescale = MP4Parser.ParseMDHD(box.Reader, box.Version); timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
}) })
.Box("minf", MP4Parser.Children) .Box("minf", MP4Parser.Children)
.Box("stbl", MP4Parser.Children) .Box("stbl", MP4Parser.Children)
.FullBox("stsd", MP4Parser.SampleDescription) .FullBox("stsd", MP4Parser.SampleDescription)
.Box("wvtt", (box) => { .Box("wvtt", _ => {
// A valid vtt init segment, though we have no actual subtitles yet. // A valid vtt init segment, though we have no actual subtitles yet.
sawWVTT = true; sawWVTT = true;
}) })
@ -38,7 +38,7 @@ namespace Mp4SubtitleParser
if (timescale == 0) if (timescale == 0)
throw new Exception("Missing timescale for VTT content!"); throw new Exception("Missing timescale for VTT content!");
List<SubCue> cues = new(); List<SubCue> cues = [];
foreach (var item in files) foreach (var item in files)
{ {
@ -50,27 +50,27 @@ namespace Mp4SubtitleParser
byte[]? rawPayload = null; byte[]? rawPayload = null;
ulong baseTime = 0; ulong baseTime = 0;
ulong defaultDuration = 0; ulong defaultDuration = 0;
List<Sample> presentations = new(); List<Sample> presentations = [];
//parse media // parse media
new MP4Parser() new MP4Parser()
.Box("moof", MP4Parser.Children) .Box("moof", MP4Parser.Children)
.Box("traf", MP4Parser.Children) .Box("traf", MP4Parser.Children)
.FullBox("tfdt", (box) => .FullBox("tfdt", box =>
{ {
sawTFDT = true; sawTFDT = true;
if (!(box.Version == 0 || box.Version == 1)) if (box.Version is not (0 or 1))
throw new Exception("TFDT version can only be 0 or 1"); throw new Exception("TFDT version can only be 0 or 1");
baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version); baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
}) })
.FullBox("tfhd", (box) => .FullBox("tfhd", box =>
{ {
if (box.Flags == 1000) if (box.Flags == 1000)
throw new Exception("A TFHD box should have a valid flags value"); throw new Exception("A TFHD box should have a valid flags value");
defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration; defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
}) })
.FullBox("trun", (box) => .FullBox("trun", box =>
{ {
sawTRUN = true; sawTRUN = true;
if (box.Version == 1000) if (box.Version == 1000)
@ -79,7 +79,7 @@ namespace Mp4SubtitleParser
throw new Exception("A TRUN box should have a valid flags value"); throw new Exception("A TRUN box should have a valid flags value");
presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData; presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
}) })
.Box("mdat", MP4Parser.AllData((data) => .Box("mdat", MP4Parser.AllData(data =>
{ {
if (sawMDAT) if (sawMDAT)
throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported"); throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported");
@ -139,13 +139,11 @@ namespace Mp4SubtitleParser
{ {
if (payload != null) if (payload != null)
{ {
if (timescale == 0)
throw new Exception("Timescale should not be zero!");
var cue = ParseVTTC( var cue = ParseVTTC(
payload, payload,
0 + (double)startTime / timescale, 0 + (double)startTime / timescale,
0 + (double)currentTime / timescale); 0 + (double)currentTime / timescale);
//Check if same subtitle has been splitted // Check if same subtitle has been splitted
if (cue != null) if (cue != null)
{ {
var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload); var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload);
@ -174,7 +172,7 @@ namespace Mp4SubtitleParser
if (reader.HasMoreData()) if (reader.HasMoreData())
{ {
//throw new Exception("MDAT which contain VTT cues and non-VTT data are not currently supported!"); // throw new Exception("MDAT which contain VTT cues and non-VTT data are not currently supported!");
} }
} }
} }
@ -192,15 +190,15 @@ namespace Mp4SubtitleParser
string id = string.Empty; string id = string.Empty;
string settings = string.Empty; string settings = string.Empty;
new MP4Parser() new MP4Parser()
.Box("payl", MP4Parser.AllData((data) => .Box("payl", MP4Parser.AllData(data =>
{ {
payload = Encoding.UTF8.GetString(data); payload = Encoding.UTF8.GetString(data);
})) }))
.Box("iden", MP4Parser.AllData((data) => .Box("iden", MP4Parser.AllData(data =>
{ {
id = Encoding.UTF8.GetString(data); id = Encoding.UTF8.GetString(data);
})) }))
.Box("sttg", MP4Parser.AllData((data) => .Box("sttg", MP4Parser.AllData(data =>
{ {
settings = Encoding.UTF8.GetString(data); settings = Encoding.UTF8.GetString(data);
})) }))
@ -212,5 +210,4 @@ namespace Mp4SubtitleParser
} }
return null; return null;
} }
}
} }

View File

@ -4,16 +4,16 @@ using N_m3u8DL_RE.Common.Util;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
//https://github.com/canalplus/rx-player/blob/48d1f845064cea5c5a3546d2c53b1855c2be149d/src/parsers/manifest/smooth/get_codecs.ts // https://github.com/canalplus/rx-player/blob/48d1f845064cea5c5a3546d2c53b1855c2be149d/src/parsers/manifest/smooth/get_codecs.ts
//https://github.dev/Dash-Industry-Forum/dash.js/blob/2aad3e79079b4de0bcd961ce6b4957103d98a621/src/mss/MssFragmentMoovProcessor.js // https://github.dev/Dash-Industry-Forum/dash.js/blob/2aad3e79079b4de0bcd961ce6b4957103d98a621/src/mss/MssFragmentMoovProcessor.js
//https://github.com/yt-dlp/yt-dlp/blob/3639df54c3298e35b5ae2a96a25bc4d3c38950d0/yt_dlp/downloader/ism.py // https://github.com/yt-dlp/yt-dlp/blob/3639df54c3298e35b5ae2a96a25bc4d3c38950d0/yt_dlp/downloader/ism.py
//https://github.com/google/ExoPlayer/blob/a9444c880230d2c2c79097e89259ce0b9f80b87d/library/extractor/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java#L38 // https://github.com/google/ExoPlayer/blob/a9444c880230d2c2c79097e89259ce0b9f80b87d/library/extractor/src/main/java/com/google/android/exoplayer2/video/HevcConfig.java#L38
//https://github.com/sannies/mp4parser/blob/master/isoparser/src/main/java/org/mp4parser/boxes/iso14496/part15/HevcDecoderConfigurationRecord.java // https://github.com/sannies/mp4parser/blob/master/isoparser/src/main/java/org/mp4parser/boxes/iso14496/part15/HevcDecoderConfigurationRecord.java
namespace N_m3u8DL_RE.Parser.Mp4 namespace N_m3u8DL_RE.Parser.Mp4;
public partial class MSSMoovProcessor
{ {
public partial class MSSMoovProcessor [GeneratedRegex(@"\<KID\>(.*?)\<")]
{
[GeneratedRegex("\\<KID\\>(.*?)\\<")]
private static partial Regex KIDRegex(); private static partial Regex KIDRegex();
private static string StartCode = "00000001"; private static string StartCode = "00000001";
@ -23,9 +23,9 @@ namespace N_m3u8DL_RE.Parser.Mp4
private string CodecPrivateData; private string CodecPrivateData;
private int Timesacle; private int Timesacle;
private long Duration; private long Duration;
private string Language { get => StreamSpec.Language ?? "und"; } private string Language => StreamSpec.Language ?? "und";
private int Width { get => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First()); } private int Width => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').First());
private int Height { get => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last()); } private int Height => int.Parse((StreamSpec.Resolution ?? "0x0").Split('x').Last());
private string StreamType; private string StreamType;
private int Channels; private int Channels;
private int BitsPerSample; private int BitsPerSample;
@ -36,8 +36,8 @@ namespace N_m3u8DL_RE.Parser.Mp4
private bool IsProtection; private bool IsProtection;
private string ProtectionSystemId; private string ProtectionSystemId;
private string ProtectionData; private string ProtectionData;
private string ProtecitonKID; private string? ProtectionKID;
private string ProtecitonKID_PR; private string? ProtectionKID_PR;
private byte[] UnityMatrix private byte[] UnityMatrix
{ {
get get
@ -60,10 +60,9 @@ namespace N_m3u8DL_RE.Parser.Mp4
private static byte TRACK_IN_MOVIE = 0x2; private static byte TRACK_IN_MOVIE = 0x2;
private static byte TRACK_IN_PREVIEW = 0x4; private static byte TRACK_IN_PREVIEW = 0x4;
private static byte SELF_CONTAINED = 0x1; private static byte SELF_CONTAINED = 0x1;
private static List<string> SupportedFourCC = new List<string>()
{ private static List<string> SupportedFourCC =
"HVC1","HEV1","AACL","AACH","EC-3","H264","AVC1","DAVC","AVC1","TTML","DVHE","DVH1" ["HVC1", "HEV1", "AACL", "AACH", "EC-3", "H264", "AVC1", "DAVC", "AVC1", "TTML", "DVHE", "DVH1"];
};
public MSSMoovProcessor(StreamSpec streamSpec) public MSSMoovProcessor(StreamSpec streamSpec)
{ {
@ -82,20 +81,20 @@ namespace N_m3u8DL_RE.Parser.Mp4
this.ProtectionData = data.ProtectionData; this.ProtectionData = data.ProtectionData;
this.ProtectionSystemId = data.ProtectionSystemID; this.ProtectionSystemId = data.ProtectionSystemID;
//需要手动生成CodecPrivateData // 需要手动生成CodecPrivateData
if (string.IsNullOrEmpty(CodecPrivateData)) if (string.IsNullOrEmpty(CodecPrivateData))
{ {
GenCodecPrivateDataForAAC(); GenCodecPrivateDataForAAC();
} }
//解析KID // 解析KID
if (IsProtection) if (IsProtection)
{ {
ExtractKID(); ExtractKID();
} }
} }
private static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = new string[] { "", "A", "B", "C" }; private static string[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = ["", "A", "B", "C"];
private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch private int SamplingFrequencyIndex(int samplingRate) => samplingRate switch
{ {
96000 => 0x0, 96000 => 0x0,
@ -116,7 +115,7 @@ namespace N_m3u8DL_RE.Parser.Mp4
private void GenCodecPrivateDataForAAC() private void GenCodecPrivateDataForAAC()
{ {
var objectType = 0x02; //AAC Main Low Complexity => object Type = 2 var objectType = 0x02; // AAC Main Low Complexity => object Type = 2
var indexFreq = SamplingFrequencyIndex(SamplingRate); var indexFreq = SamplingFrequencyIndex(SamplingRate);
if (FourCC == "AACH") if (FourCC == "AACH")
@ -127,17 +126,17 @@ namespace N_m3u8DL_RE.Parser.Mp4
var codecPrivateData = new byte[4]; var codecPrivateData = new byte[4];
var extensionSamplingFrequencyIndex = SamplingFrequencyIndex(SamplingRate * 2); // in HE AAC Extension Sampling frequence var extensionSamplingFrequencyIndex = SamplingFrequencyIndex(SamplingRate * 2); // in HE AAC Extension Sampling frequence
// equals to SamplingRate*2 // equals to SamplingRate*2
//Freq Index is present for 3 bits in the first byte, last bit is in the second // Freq Index is present for 3 bits in the first byte, last bit is in the second
codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1)); codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1));
codecPrivateData[1] = (byte)((indexFreq << 7) | (Channels << 3) | (extensionSamplingFrequencyIndex >> 1)); codecPrivateData[1] = (byte)((indexFreq << 7) | (Channels << 3) | (extensionSamplingFrequencyIndex >> 1));
codecPrivateData[2] = (byte)((extensionSamplingFrequencyIndex << 7) | (0x02 << 2)); // origin object type equals to 2 => AAC Main Low Complexity codecPrivateData[2] = (byte)((extensionSamplingFrequencyIndex << 7) | (0x02 << 2)); // origin object type equals to 2 => AAC Main Low Complexity
codecPrivateData[3] = 0x0; //alignment bits codecPrivateData[3] = 0x0; // alignment bits
var arr16 = new ushort[2]; var arr16 = new ushort[2];
arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]); arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]);
arr16[1] = (ushort)((codecPrivateData[2] << 8) + codecPrivateData[3]); arr16[1] = (ushort)((codecPrivateData[2] << 8) + codecPrivateData[3]);
//convert decimal to hex value // convert decimal to hex value
this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0'); this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0');
this.CodecPrivateData += HexUtil.BytesToHex(BitConverter.GetBytes(arr16[1])).PadLeft(16, '0'); this.CodecPrivateData += HexUtil.BytesToHex(BitConverter.GetBytes(arr16[1])).PadLeft(16, '0');
} }
@ -146,36 +145,36 @@ namespace N_m3u8DL_RE.Parser.Mp4
// 2 bytes : XXXXX XXXX XXXX XXX // 2 bytes : XXXXX XXXX XXXX XXX
// ' ObjectType' 'Freq Index' 'Channels value' 'GAS = 000' // ' ObjectType' 'Freq Index' 'Channels value' 'GAS = 000'
var codecPrivateData = new byte[2]; var codecPrivateData = new byte[2];
//Freq Index is present for 3 bits in the first byte, last bit is in the second // Freq Index is present for 3 bits in the first byte, last bit is in the second
codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1)); codecPrivateData[0] = (byte)((objectType << 3) | (indexFreq >> 1));
codecPrivateData[1] = (byte)((indexFreq << 7) | Channels << 3); codecPrivateData[1] = (byte)((indexFreq << 7) | Channels << 3);
// put the 2 bytes in an 16 bits array // put the 2 bytes in an 16 bits array
var arr16 = new ushort[1]; var arr16 = new ushort[1];
arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]); arr16[0] = (ushort)((codecPrivateData[0] << 8) + codecPrivateData[1]);
//convert decimal to hex value // convert decimal to hex value
this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0'); this.CodecPrivateData = HexUtil.BytesToHex(BitConverter.GetBytes(arr16[0])).PadLeft(16, '0');
} }
} }
private void ExtractKID() private void ExtractKID()
{ {
//playready // playready
if (ProtectionSystemId.ToUpper() == "9A04F079-9840-4286-AB92-E65BE0885F95") if (ProtectionSystemId.ToUpper() == "9A04F079-9840-4286-AB92-E65BE0885F95")
{ {
var bytes = HexUtil.HexToBytes(ProtectionData.Replace("00", "")); var bytes = HexUtil.HexToBytes(ProtectionData.Replace("00", ""));
var text = Encoding.ASCII.GetString(bytes); var text = Encoding.ASCII.GetString(bytes);
var kidBytes = Convert.FromBase64String(KIDRegex().Match(text).Groups[1].Value); var kidBytes = Convert.FromBase64String(KIDRegex().Match(text).Groups[1].Value);
//save kid for playready // save kid for playready
this.ProtecitonKID_PR = HexUtil.BytesToHex(kidBytes); this.ProtectionKID_PR = HexUtil.BytesToHex(kidBytes);
//fix byte order // fix byte order
var reverse1 = new byte[4] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] }; var reverse1 = new[] { kidBytes[3], kidBytes[2], kidBytes[1], kidBytes[0] };
var reverse2 = new byte[4] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] }; var reverse2 = new[] { kidBytes[5], kidBytes[4], kidBytes[7], kidBytes[6] };
Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length); Array.Copy(reverse1, 0, kidBytes, 0, reverse1.Length);
Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length); Array.Copy(reverse2, 0, kidBytes, 4, reverse1.Length);
this.ProtecitonKID = HexUtil.BytesToHex(kidBytes); this.ProtectionKID = HexUtil.BytesToHex(kidBytes);
} }
//widevine // widevine
else if (ProtectionSystemId.ToUpper() == "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED") else if (ProtectionSystemId.ToUpper() == "EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED")
{ {
throw new NotSupportedException(); throw new NotSupportedException();
@ -216,19 +215,19 @@ namespace N_m3u8DL_RE.Parser.Mp4
sinfPayload.AddRange(frmaBox); sinfPayload.AddRange(frmaBox);
var schmPayload = new List<byte>(); var schmPayload = new List<byte>();
schmPayload.AddRange(Encoding.ASCII.GetBytes("cenc")); //scheme_type 'cenc' => common encryption schmPayload.AddRange(Encoding.ASCII.GetBytes("cenc")); // scheme_type 'cenc' => common encryption
schmPayload.AddRange(new byte[] { 0, 1, 0, 0 }); //scheme_version Major version 1, Minor version 0 schmPayload.AddRange([0, 1, 0, 0]); // scheme_version Major version 1, Minor version 0
var schmBox = FullBox("schm", 0, 0, schmPayload.ToArray()); var schmBox = FullBox("schm", 0, 0, schmPayload.ToArray());
sinfPayload.AddRange(schmBox); sinfPayload.AddRange(schmBox);
var tencPayload = new List<byte>(); var tencPayload = new List<byte>();
tencPayload.AddRange(new byte[] { 0, 0 }); tencPayload.AddRange([0, 0]);
tencPayload.Add(0x1); //default_IsProtected tencPayload.Add(0x1); // default_IsProtected
tencPayload.Add(0x8); //default_Per_Sample_IV_size tencPayload.Add(0x8); // default_Per_Sample_IV_size
tencPayload.AddRange(HexUtil.HexToBytes(ProtecitonKID)); //default_KID tencPayload.AddRange(HexUtil.HexToBytes(ProtectionKID)); // default_KID
//tencPayload.Add(0x8);//default_constant_IV_size // tencPayload.Add(0x8);// default_constant_IV_size
//tencPayload.AddRange(new byte[8]);//default_constant_IV // tencPayload.AddRange(new byte[8]);// default_constant_IV
var tencBox = FullBox("tenc", 0, 0, tencPayload.ToArray()); var tencBox = FullBox("tenc", 0, 0, tencPayload.ToArray());
var schiBox = Box("schi", tencBox); var schiBox = Box("schi", tencBox);
@ -244,12 +243,12 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.Write("isml"); //major brand writer.Write("isml"); // major brand
writer.WriteUInt(1); //minor version writer.WriteUInt(1); // minor version
writer.Write("iso5"); //compatible brand writer.Write("iso5"); // compatible brand
writer.Write("iso6"); //compatible brand writer.Write("iso6"); // compatible brand
writer.Write("piff"); //compatible brand writer.Write("piff"); // compatible brand
writer.Write("msdh"); //compatible brand writer.Write("msdh"); // compatible brand
return Box("ftyp", stream.ToArray()); return Box("ftyp", stream.ToArray());
} }
@ -259,26 +258,26 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteULong(CreationTime); //creation_time writer.WriteULong(CreationTime); // creation_time
writer.WriteULong(CreationTime); //modification_time writer.WriteULong(CreationTime); // modification_time
writer.WriteUInt(Timesacle); //timescale writer.WriteUInt(Timesacle); // timescale
writer.WriteULong(Duration); //duration writer.WriteULong(Duration); // duration
writer.WriteUShort(1, padding: 2); //rate writer.WriteUShort(1, padding: 2); // rate
writer.WriteByte(1, padding: 1); //volume writer.WriteByte(1, padding: 1); // volume
writer.WriteUShort(0); //reserved writer.WriteUShort(0); // reserved
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0); writer.WriteUInt(0);
writer.Write(UnityMatrix); writer.Write(UnityMatrix);
writer.WriteUInt(0); //pre defined writer.WriteUInt(0); // pre defined
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0xffffffff); //next track id writer.WriteUInt(0xffffffff); // next track id
return FullBox("mvhd", 1, 0, stream.ToArray()); return FullBox("mvhd", 1, 0, stream.ToArray());
@ -289,22 +288,22 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteULong(CreationTime); //creation_time writer.WriteULong(CreationTime); // creation_time
writer.WriteULong(CreationTime); //modification_time writer.WriteULong(CreationTime); // modification_time
writer.WriteUInt(TrackId); //track id writer.WriteUInt(TrackId); // track id
writer.WriteUInt(0); //reserved writer.WriteUInt(0); // reserved
writer.WriteULong(Duration); //duration writer.WriteULong(Duration); // duration
writer.WriteUInt(0); //reserved writer.WriteUInt(0); // reserved
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteShort(0); //layer writer.WriteShort(0); // layer
writer.WriteShort(0); //alternate group writer.WriteShort(0); // alternate group
writer.WriteByte(StreamType == "audio" ? (byte)1 : (byte)0, padding: 1); //volume writer.WriteByte(StreamType == "audio" ? (byte)1 : (byte)0, padding: 1); // volume
writer.WriteUShort(0); //reserved writer.WriteUShort(0); // reserved
writer.Write(UnityMatrix); writer.Write(UnityMatrix);
writer.WriteUShort(Width, padding: 2); //width writer.WriteUShort(Width, padding: 2); // width
writer.WriteUShort(Height, padding: 2); //height writer.WriteUShort(Height, padding: 2); // height
return FullBox("tkhd", 1, (uint)TRACK_ENABLED | TRACK_IN_MOVIE | TRACK_IN_PREVIEW, stream.ToArray()); return FullBox("tkhd", 1, (uint)TRACK_ENABLED | TRACK_IN_MOVIE | TRACK_IN_PREVIEW, stream.ToArray());
} }
@ -315,12 +314,12 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteULong(CreationTime); //creation_time writer.WriteULong(CreationTime); // creation_time
writer.WriteULong(CreationTime); //modification_time writer.WriteULong(CreationTime); // modification_time
writer.WriteUInt(Timesacle); //timescale writer.WriteUInt(Timesacle); // timescale
writer.WriteULong(Duration); //duration writer.WriteULong(Duration); // duration
writer.WriteUShort((Language[0] - 0x60) << 10 | (Language[1] - 0x60) << 5 | (Language[2] - 0x60)); //language writer.WriteUShort((Language[0] - 0x60) << 10 | (Language[1] - 0x60) << 5 | (Language[2] - 0x60)); // language
writer.WriteUShort(0); //pre defined writer.WriteUShort(0); // pre defined
return FullBox("mdhd", 1, 0, stream.ToArray()); return FullBox("mdhd", 1, 0, stream.ToArray());
} }
@ -330,16 +329,16 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteUInt(0); //pre defined writer.WriteUInt(0); // pre defined
if (StreamType == "audio") writer.Write("soun"); if (StreamType == "audio") writer.Write("soun");
else if (StreamType == "video") writer.Write("vide"); else if (StreamType == "video") writer.Write("vide");
else if (StreamType == "text") writer.Write("subt"); else if (StreamType == "text") writer.Write("subt");
else throw new NotSupportedException(); else throw new NotSupportedException();
writer.WriteUInt(0); //reserved writer.WriteUInt(0); // reserved
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0); writer.WriteUInt(0);
writer.Write($"{StreamSpec.GroupId ?? "RE Handler"}\0"); //name writer.Write($"{StreamSpec.GroupId ?? "RE Handler"}\0"); // name
return FullBox("hdlr", 0, 0, stream.ToArray()); return FullBox("hdlr", 0, 0, stream.ToArray());
} }
@ -353,22 +352,22 @@ namespace N_m3u8DL_RE.Parser.Mp4
if (StreamType == "audio") if (StreamType == "audio")
{ {
var smhd = new List<byte>(); var smhd = new List<byte>();
smhd.Add(0); smhd.Add(0); //balance smhd.Add(0); smhd.Add(0); // balance
smhd.Add(0); smhd.Add(0); //reserved smhd.Add(0); smhd.Add(0); // reserved
minfPayload.AddRange(FullBox("smhd", 0, 0, smhd.ToArray())); //Sound Media Header minfPayload.AddRange(FullBox("smhd", 0, 0, smhd.ToArray())); // Sound Media Header
} }
else if (StreamType == "video") else if (StreamType == "video")
{ {
var vmhd = new List<byte>(); var vmhd = new List<byte>();
vmhd.Add(0); vmhd.Add(0); //graphics mode vmhd.Add(0); vmhd.Add(0); // graphics mode
vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0);//opcolor vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0); vmhd.Add(0);// opcolor
minfPayload.AddRange(FullBox("vmhd", 0, 1, vmhd.ToArray())); //Video Media Header minfPayload.AddRange(FullBox("vmhd", 0, 1, vmhd.ToArray())); // Video Media Header
} }
else if (StreamType == "text") else if (StreamType == "text")
{ {
minfPayload.AddRange(FullBox("sthd", 0, 0, new byte[0])); //Subtitle Media Header minfPayload.AddRange(FullBox("sthd", 0, 0, [])); // Subtitle Media Header
} }
else else
{ {
@ -376,11 +375,11 @@ namespace N_m3u8DL_RE.Parser.Mp4
} }
var drefPayload = new List<byte>(); var drefPayload = new List<byte>();
drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); //entry count drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(0); drefPayload.Add(1); // entry count
drefPayload.AddRange(FullBox("url ", 0, SELF_CONTAINED, new byte[0])); //Data Entry URL Box drefPayload.AddRange(FullBox("url ", 0, SELF_CONTAINED, [])); // Data Entry URL Box
var dinfPayload = FullBox("dref", 0, 0, drefPayload.ToArray()); //Data Reference Box var dinfPayload = FullBox("dref", 0, 0, drefPayload.ToArray()); // Data Reference Box
minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); //Data Information Box minfPayload.AddRange(Box("dinf", dinfPayload.ToArray())); // Data Information Box
return minfPayload.ToArray(); return minfPayload.ToArray();
} }
@ -398,35 +397,35 @@ namespace N_m3u8DL_RE.Parser.Mp4
// esdsLength = 34 + len(audioSpecificConfig) // esdsLength = 34 + len(audioSpecificConfig)
// ES_Descriptor (see ISO/IEC 14496-1 (Systems)) // ES_Descriptor (see ISO/IEC 14496-1 (Systems))
writer.WriteByte(0x03); //tag = 0x03 (ES_DescrTag) writer.WriteByte(0x03); // tag = 0x03 (ES_DescrTag)
writer.WriteByte((byte)(20 + audioSpecificConfig.Length)); //size writer.WriteByte((byte)(20 + audioSpecificConfig.Length)); // size
writer.WriteByte((byte)((TrackId & 0xFF00) >> 8)); //ES_ID = track_id writer.WriteByte((byte)((TrackId & 0xFF00) >> 8)); // ES_ID = track_id
writer.WriteByte((byte)(TrackId & 0x00FF)); writer.WriteByte((byte)(TrackId & 0x00FF));
writer.WriteByte(0); //flags and streamPriority writer.WriteByte(0); // flags and streamPriority
// DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems)) // DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems))
writer.WriteByte(0x04); //tag = 0x04 (DecoderConfigDescrTag) writer.WriteByte(0x04); // tag = 0x04 (DecoderConfigDescrTag)
writer.WriteByte((byte)(15 + audioSpecificConfig.Length)); //size writer.WriteByte((byte)(15 + audioSpecificConfig.Length)); // size
writer.WriteByte(0x40); //objectTypeIndication = 0x40 (MPEG-4 AAC) writer.WriteByte(0x40); // objectTypeIndication = 0x40 (MPEG-4 AAC)
writer.WriteByte((0x05 << 2) | (0 << 1) | 1); //reserved = 1 writer.WriteByte((0x05 << 2) | (0 << 1) | 1); // reserved = 1
writer.WriteByte(0xFF); //buffersizeDB = undefined writer.WriteByte(0xFF); // buffersizeDB = undefined
writer.WriteByte(0xFF); writer.WriteByte(0xFF);
writer.WriteByte(0xFF); writer.WriteByte(0xFF);
var bandwidth = StreamSpec.Bandwidth!; var bandwidth = StreamSpec.Bandwidth!;
writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); //maxBitrate writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); // maxBitrate
writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16)); writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16));
writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8)); writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8));
writer.WriteByte((byte)(bandwidth & 0x000000FF)); writer.WriteByte((byte)(bandwidth & 0x000000FF));
writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); //avgbitrate writer.WriteByte((byte)((bandwidth & 0xFF000000) >> 24)); // avgbitrate
writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16)); writer.WriteByte((byte)((bandwidth & 0x00FF0000) >> 16));
writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8)); writer.WriteByte((byte)((bandwidth & 0x0000FF00) >> 8));
writer.WriteByte((byte)(bandwidth & 0x000000FF)); writer.WriteByte((byte)(bandwidth & 0x000000FF));
// DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems)) // DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems))
writer.WriteByte(0x05); //tag = 0x05 (DecSpecificInfoTag) writer.WriteByte(0x05); // tag = 0x05 (DecSpecificInfoTag)
writer.WriteByte((byte)audioSpecificConfig.Length); //size writer.WriteByte((byte)audioSpecificConfig.Length); // size
writer.Write(audioSpecificConfig); //AudioSpecificConfig bytes writer.Write(audioSpecificConfig); // AudioSpecificConfig bytes
return FullBox("esds", 0, 0, stream.ToArray()); return FullBox("esds", 0, 0, stream.ToArray());
} }
@ -436,23 +435,23 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteByte(0); //reserved writer.WriteByte(0); // reserved
writer.WriteByte(0); writer.WriteByte(0);
writer.WriteByte(0); writer.WriteByte(0);
writer.WriteByte(0); writer.WriteByte(0);
writer.WriteByte(0); writer.WriteByte(0);
writer.WriteByte(0); writer.WriteByte(0);
writer.WriteUShort(1); //data reference index writer.WriteUShort(1); // data reference index
if (StreamType == "audio") if (StreamType == "audio")
{ {
writer.WriteUInt(0); //reserved2 writer.WriteUInt(0); // reserved2
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUShort(Channels); //channels writer.WriteUShort(Channels); // channels
writer.WriteUShort(BitsPerSample); //bits_per_sample writer.WriteUShort(BitsPerSample); // bits_per_sample
writer.WriteUShort(0); //pre defined writer.WriteUShort(0); // pre defined
writer.WriteUShort(0); //reserved3 writer.WriteUShort(0); // reserved3
writer.WriteUShort(SamplingRate, padding: 2); //sampling_rate writer.WriteUShort(SamplingRate, padding: 2); // sampling_rate
var audioSpecificConfig = HexUtil.HexToBytes(CodecPrivateData); var audioSpecificConfig = HexUtil.HexToBytes(CodecPrivateData);
var esdsBox = GenEsds(audioSpecificConfig); var esdsBox = GenEsds(audioSpecificConfig);
@ -464,128 +463,108 @@ namespace N_m3u8DL_RE.Parser.Mp4
{ {
var sinfBox = GenSinf("mp4a"); var sinfBox = GenSinf("mp4a");
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("enca", stream.ToArray()); //Encrypted Audio return Box("enca", stream.ToArray()); // Encrypted Audio
} }
else
{
return Box("mp4a", stream.ToArray()); return Box("mp4a", stream.ToArray());
} }
}
if (FourCC == "EC-3") if (FourCC == "EC-3")
{ {
if (IsProtection) if (IsProtection)
{ {
var sinfBox = GenSinf("ec-3"); var sinfBox = GenSinf("ec-3");
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("enca", stream.ToArray()); //Encrypted Audio return Box("enca", stream.ToArray()); // Encrypted Audio
} }
else
{
return Box("ec-3", stream.ToArray()); return Box("ec-3", stream.ToArray());
} }
} }
}
else if (StreamType == "video") else if (StreamType == "video")
{ {
writer.WriteUShort(0); //pre defined writer.WriteUShort(0); // pre defined
writer.WriteUShort(0); //reserved writer.WriteUShort(0); // reserved
writer.WriteUInt(0); //pre defined writer.WriteUInt(0); // pre defined
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUInt(0); writer.WriteUInt(0);
writer.WriteUShort(Width); //width writer.WriteUShort(Width); // width
writer.WriteUShort(Height); //height writer.WriteUShort(Height); // height
writer.WriteUShort(0x48, padding: 2); //horiz resolution 72 dpi writer.WriteUShort(0x48, padding: 2); // horiz resolution 72 dpi
writer.WriteUShort(0x48, padding: 2); //vert resolution 72 dpi writer.WriteUShort(0x48, padding: 2); // vert resolution 72 dpi
writer.WriteUInt(0); //reserved writer.WriteUInt(0); // reserved
writer.WriteUShort(1); //frame count writer.WriteUShort(1); // frame count
for (int i = 0; i < 32; i++) //compressor name for (int i = 0; i < 32; i++) // compressor name
{ {
writer.WriteByte(0); writer.WriteByte(0);
} }
writer.WriteUShort(0x18); //depth writer.WriteUShort(0x18); // depth
writer.WriteUShort(65535); //pre defined writer.WriteUShort(65535); // pre defined
var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData); var codecPrivateData = HexUtil.HexToBytes(CodecPrivateData);
if (FourCC == "H264" || FourCC == "AVC1" || FourCC == "DAVC" || FourCC == "AVC1") if (FourCC is "H264" or "AVC1" or "DAVC")
{ {
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7).First()); var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 7));
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8).First()); var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] & 0x1F) == 8));
//make avcC // make avcC
var avcC = GetAvcC(sps, pps); var avcC = GetAvcC(sps, pps);
writer.Write(avcC); writer.Write(avcC);
if (IsProtection) if (IsProtection)
{ {
var sinfBox = GenSinf("avc1"); var sinfBox = GenSinf("avc1");
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("encv", stream.ToArray()); //Encrypted Video return Box("encv", stream.ToArray()); // Encrypted Video
} }
else return Box("avc1", stream.ToArray()); // AVC Simple Entry
}
if (FourCC is "HVC1" or "HEV1")
{ {
return Box("avc1", stream.ToArray()); //AVC Simple Entry var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
} var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));
} var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));
else if (FourCC == "HVC1" || FourCC == "HEV1") var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));
{ // make hvcC
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries);
var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First());
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First());
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First());
//make hvcC
var hvcC = GetHvcC(sps, pps, vps); var hvcC = GetHvcC(sps, pps, vps);
writer.Write(hvcC); writer.Write(hvcC);
if (IsProtection) if (IsProtection)
{ {
var sinfBox = GenSinf("hvc1"); var sinfBox = GenSinf("hvc1");
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("encv", stream.ToArray()); //Encrypted Video return Box("encv", stream.ToArray()); // Encrypted Video
}
else
{
return Box("hvc1", stream.ToArray()); //HEVC Simple Entry
} }
return Box("hvc1", stream.ToArray()); // HEVC Simple Entry
} }
// 杜比视界也按照hevc处理 // 杜比视界也按照hevc处理
else if (FourCC == "DVHE" || FourCC == "DVH1") if (FourCC is "DVHE" or "DVH1")
{ {
var arr = CodecPrivateData.Split(new[] { StartCode }, StringSplitOptions.RemoveEmptyEntries); var arr = CodecPrivateData.Split([StartCode], StringSplitOptions.RemoveEmptyEntries);
var vps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20).First()); var vps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x20));
var sps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21).First()); var sps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x21));
var pps = HexUtil.HexToBytes(arr.Where(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22).First()); var pps = HexUtil.HexToBytes(arr.First(x => (HexUtil.HexToBytes(x[0..2])[0] >> 1) == 0x22));
//make hvcC // make hvcC
var hvcC = GetHvcC(sps, pps, vps, "dvh1"); var hvcC = GetHvcC(sps, pps, vps, "dvh1");
writer.Write(hvcC); writer.Write(hvcC);
if (IsProtection) if (IsProtection)
{ {
var sinfBox = GenSinf("dvh1"); var sinfBox = GenSinf("dvh1");
writer.Write(sinfBox); writer.Write(sinfBox);
return Box("encv", stream.ToArray()); //Encrypted Video return Box("encv", stream.ToArray()); // Encrypted Video
} }
else return Box("dvh1", stream.ToArray()); // HEVC Simple Entry
{
return Box("dvh1", stream.ToArray()); //HEVC Simple Entry
} }
}
else
{
throw new NotSupportedException(); throw new NotSupportedException();
} }
}
else if (StreamType == "text") else if (StreamType == "text")
{ {
if (FourCC == "TTML") if (FourCC == "TTML")
{ {
writer.Write("http://www.w3.org/ns/ttml\0"); //namespace writer.Write("http://www.w3.org/ns/ttml\0"); // namespace
writer.Write("\0"); //schema location writer.Write("\0"); // schema location
writer.Write("\0"); //auxilary mime types(??) writer.Write("\0"); // auxilary mime types(??)
return Box("stpp", stream.ToArray()); //TTML Simple Entry return Box("stpp", stream.ToArray()); // TTML Simple Entry
} }
else
{
throw new NotSupportedException(); throw new NotSupportedException();
} }
}
else else
{ {
throw new NotSupportedException(); throw new NotSupportedException();
@ -599,26 +578,26 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteByte(1); //configuration version writer.WriteByte(1); // configuration version
writer.Write(sps[1..4]); //avc profile indication + profile compatibility + avc level indication writer.Write(sps[1..4]); // avc profile indication + profile compatibility + avc level indication
writer.WriteByte((byte)(0xfc | (NalUnitLengthField - 1))); //complete representation (1) + reserved (11111) + length size minus one writer.WriteByte((byte)(0xfc | (NalUnitLengthField - 1))); // complete representation (1) + reserved (11111) + length size minus one
writer.WriteByte(1); //reserved (0) + number of sps (0000001) writer.WriteByte(1); // reserved (0) + number of sps (0000001)
writer.WriteUShort(sps.Length); writer.WriteUShort(sps.Length);
writer.Write(sps); writer.Write(sps);
writer.WriteByte(1); //number of pps writer.WriteByte(1); // number of pps
writer.WriteUShort(pps.Length); writer.WriteUShort(pps.Length);
writer.Write(pps); writer.Write(pps);
return Box("avcC", stream.ToArray()); //AVC Decoder Configuration Record return Box("avcC", stream.ToArray()); // AVC Decoder Configuration Record
} }
private byte[] GetHvcC(byte[] sps, byte[] pps, byte[] vps, string code = "hvc1") private byte[] GetHvcC(byte[] sps, byte[] pps, byte[] vps, string code = "hvc1")
{ {
var oriSps = new List<byte>(sps); var oriSps = new List<byte>(sps);
//https://www.itu.int/rec/dologin.asp?lang=f&id=T-REC-H.265-201504-S!!PDF-E&type=items // https://www.itu.int/rec/dologin.asp?lang=f&id=T-REC-H.265-201504-S!!PDF-E&type=items
//Read generalProfileSpace, generalTierFlag, generalProfileIdc, // Read generalProfileSpace, generalTierFlag, generalProfileIdc,
//generalProfileCompatibilityFlags, constraintBytes, generalLevelIdc // generalProfileCompatibilityFlags, constraintBytes, generalLevelIdc
//from sps // from sps
var encList = new List<byte>(); var encList = new List<byte>();
/** /**
* payload, 00 00 03 0,1,2,3 00 00 XX 03 * payload, 00 00 03 0,1,2,3 00 00 XX 03
@ -635,7 +614,7 @@ namespace N_m3u8DL_RE.Parser.Mp4
while (_reader.BaseStream.Position < _reader.BaseStream.Length) while (_reader.BaseStream.Position < _reader.BaseStream.Length)
{ {
encList.Add(_reader.ReadByte()); encList.Add(_reader.ReadByte());
if (encList.Count >= 3 && encList[encList.Count - 3] == 0x00 && encList[encList.Count - 2] == 0x00 && encList[encList.Count - 1] == 0x03) if (encList is [.., 0x00, 0x00, 0x03])
{ {
encList.RemoveAt(encList.Count - 1); encList.RemoveAt(encList.Count - 1);
} }
@ -644,7 +623,7 @@ namespace N_m3u8DL_RE.Parser.Mp4
sps = encList.ToArray(); sps = encList.ToArray();
using var reader = new BinaryReader2(new MemoryStream(sps)); using var reader = new BinaryReader2(new MemoryStream(sps));
reader.ReadBytes(2); //Skip 2 bytes unit header reader.ReadBytes(2); // Skip 2 bytes unit header
var firstByte = reader.ReadByte(); var firstByte = reader.ReadByte();
var maxSubLayersMinus1 = (firstByte & 0xe) >> 1; var maxSubLayersMinus1 = (firstByte & 0xe) >> 1;
var nextByte = reader.ReadByte(); var nextByte = reader.ReadByte();
@ -658,21 +637,21 @@ namespace N_m3u8DL_RE.Parser.Mp4
/*var skipBit = 0; /*var skipBit = 0;
for (int i = 0; i < maxSubLayersMinus1; i++) for (int i = 0; i < maxSubLayersMinus1; i++)
{ {
skipBit += 2; //sub_layer_profile_present_flag sub_layer_level_present_flag skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag
} }
if (maxSubLayersMinus1 > 0) if (maxSubLayersMinus1 > 0)
{ {
for (int i = maxSubLayersMinus1; i < 8; i++) for (int i = maxSubLayersMinus1; i < 8; i++)
{ {
skipBit += 2; //reserved_zero_2bits skipBit += 2; // reserved_zero_2bits
} }
} }
for (int i = 0; i < maxSubLayersMinus1; i++) for (int i = 0; i < maxSubLayersMinus1; i++)
{ {
skipBit += 2; //sub_layer_profile_present_flag sub_layer_level_present_flag skipBit += 2; // sub_layer_profile_present_flag sub_layer_level_present_flag
}*/ }*/
//生成编码信息 // 生成编码信息
var codecs = code + var codecs = code +
$".{HEVC_GENERAL_PROFILE_SPACE_STRINGS[generalProfileSpace]}{generalProfileIdc}" + $".{HEVC_GENERAL_PROFILE_SPACE_STRINGS[generalProfileSpace]}{generalProfileIdc}" +
$".{Convert.ToString(generalProfileCompatibilityFlags, 16)}" + $".{Convert.ToString(generalProfileCompatibilityFlags, 16)}" +
@ -687,37 +666,37 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
//var reserved1 = 0xF; // var reserved1 = 0xF;
writer.WriteByte(1); //configuration version writer.WriteByte(1); // configuration version
writer.WriteByte((byte)((generalProfileSpace << 6) + (generalTierFlag == 1 ? 0x20 : 0) | generalProfileIdc)); //general_profile_space + general_tier_flag + general_profile_idc writer.WriteByte((byte)((generalProfileSpace << 6) + (generalTierFlag == 1 ? 0x20 : 0) | generalProfileIdc)); // general_profile_space + general_tier_flag + general_profile_idc
writer.WriteUInt(generalProfileCompatibilityFlags); //general_profile_compatibility_flags writer.WriteUInt(generalProfileCompatibilityFlags); // general_profile_compatibility_flags
writer.Write(constraintBytes); //general_constraint_indicator_flags writer.Write(constraintBytes); // general_constraint_indicator_flags
writer.WriteByte((byte)generalProfileIdc); //general_level_idc writer.WriteByte((byte)generalProfileIdc); // general_level_idc
writer.WriteUShort(0xf000); //reserved + min_spatial_segmentation_idc writer.WriteUShort(0xf000); // reserved + min_spatial_segmentation_idc
writer.WriteByte(0xfc); //reserved + parallelismType writer.WriteByte(0xfc); // reserved + parallelismType
writer.WriteByte(0 | 0xfc); //reserved + chromaFormat writer.WriteByte(0 | 0xfc); // reserved + chromaFormat
writer.WriteByte(0 | 0xf8); //reserved + bitDepthLumaMinus8 writer.WriteByte(0 | 0xf8); // reserved + bitDepthLumaMinus8
writer.WriteByte(0 | 0xf8); //reserved + bitDepthChromaMinus8 writer.WriteByte(0 | 0xf8); // reserved + bitDepthChromaMinus8
writer.WriteUShort(0); //avgFrameRate writer.WriteUShort(0); // avgFrameRate
writer.WriteByte((byte)(0 << 6 | 0 << 3 | 0 << 2 | (NalUnitLengthField - 1))); //constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne writer.WriteByte((byte)(0 << 6 | 0 << 3 | 0 << 2 | (NalUnitLengthField - 1))); // constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne
writer.WriteByte(0x03); //numOfArrays (vps sps pps) writer.WriteByte(0x03); // numOfArrays (vps sps pps)
sps = oriSps.ToArray(); sps = oriSps.ToArray();
writer.WriteByte(0x20); //array_completeness + reserved + NAL_unit_type writer.WriteByte(0x20); // array_completeness + reserved + NAL_unit_type
writer.WriteUShort(1); //numNalus writer.WriteUShort(1); // numNalus
writer.WriteUShort(vps.Length); writer.WriteUShort(vps.Length);
writer.Write(vps); writer.Write(vps);
writer.WriteByte(0x21); writer.WriteByte(0x21);
writer.WriteUShort(1); //numNalus writer.WriteUShort(1); // numNalus
writer.WriteUShort(sps.Length); writer.WriteUShort(sps.Length);
writer.Write(sps); writer.Write(sps);
writer.WriteByte(0x22); writer.WriteByte(0x22);
writer.WriteUShort(1); //numNalus writer.WriteUShort(1); // numNalus
writer.WriteUShort(pps.Length); writer.WriteUShort(pps.Length);
writer.Write(pps); writer.Write(pps);
return Box("hvcC", stream.ToArray()); //HEVC Decoder Configuration Record return Box("hvcC", stream.ToArray()); // HEVC Decoder Configuration Record
} }
private byte[] GetStsd() private byte[] GetStsd()
@ -725,7 +704,7 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteUInt(1); //entry count writer.WriteUInt(1); // entry count
var sampleEntryData = GetSampleEntryBox(); var sampleEntryData = GetSampleEntryBox();
writer.Write(sampleEntryData); writer.Write(sampleEntryData);
@ -739,20 +718,20 @@ namespace N_m3u8DL_RE.Parser.Mp4
writer.WriteULong(Duration); writer.WriteULong(Duration);
return FullBox("mehd", 1, 0, stream.ToArray()); //Movie Extends Header Box return FullBox("mehd", 1, 0, stream.ToArray()); // Movie Extends Header Box
} }
private byte[] GetTrex() private byte[] GetTrex()
{ {
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
writer.WriteUInt(TrackId); //track id writer.WriteUInt(TrackId); // track id
writer.WriteUInt(1); //default sample description index writer.WriteUInt(1); // default sample description index
writer.WriteUInt(0); //default sample duration writer.WriteUInt(0); // default sample duration
writer.WriteUInt(0); //default sample size writer.WriteUInt(0); // default sample size
writer.WriteUInt(0); //default sample flags writer.WriteUInt(0); // default sample flags
return FullBox("trex", 0, 0, stream.ToArray()); //Track Extends Box return FullBox("trex", 0, 0, stream.ToArray()); // Track Extends Box
} }
private byte[] GenPsshBoxForPlayReady() private byte[] GenPsshBoxForPlayReady()
@ -763,8 +742,8 @@ namespace N_m3u8DL_RE.Parser.Mp4
var psshData = HexUtil.HexToBytes(ProtectionData); var psshData = HexUtil.HexToBytes(ProtectionData);
_writer.Write(sysIdData); // SystemID 16 bytes _writer.Write(sysIdData); // SystemID 16 bytes
_writer.WriteUInt(psshData.Length); //Size of Data 4 bytes _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes
_writer.Write(psshData); //Data _writer.Write(psshData); // Data
var psshBox = FullBox("pssh", 0, 0, _stream.ToArray()); var psshBox = FullBox("pssh", 0, 0, _stream.ToArray());
return psshBox; return psshBox;
} }
@ -774,12 +753,12 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var _stream = new MemoryStream(); using var _stream = new MemoryStream();
using var _writer = new BinaryWriter2(_stream); using var _writer = new BinaryWriter2(_stream);
var sysIdData = HexUtil.HexToBytes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed".Replace("-", "")); var sysIdData = HexUtil.HexToBytes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed".Replace("-", ""));
//var kid = HexUtil.HexToBytes(ProtecitonKID); // var kid = HexUtil.HexToBytes(ProtectionKID);
_writer.Write(sysIdData); // SystemID 16 bytes _writer.Write(sysIdData); // SystemID 16 bytes
var psshData = HexUtil.HexToBytes($"08011210{ProtecitonKID}1A046E647265220400000000"); var psshData = HexUtil.HexToBytes($"08011210{ProtectionKID}1A046E647265220400000000");
_writer.WriteUInt(psshData.Length); //Size of Data 4 bytes _writer.WriteUInt(psshData.Length); // Size of Data 4 bytes
_writer.Write(psshData); //Data _writer.Write(psshData); // Data
var psshBox = FullBox("pssh", 0, 0, _stream.ToArray()); var psshBox = FullBox("pssh", 0, 0, _stream.ToArray());
return psshBox; return psshBox;
} }
@ -789,13 +768,13 @@ namespace N_m3u8DL_RE.Parser.Mp4
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using var writer = new BinaryWriter2(stream); using var writer = new BinaryWriter2(stream);
//make senc // make senc
writer.WriteUInt(1); //sample_count writer.WriteUInt(1); // sample_count
writer.Write(new byte[8]); //8 bytes IV writer.Write(new byte[8]); // 8 bytes IV
var sencBox = FullBox("senc", 1, 0, stream.ToArray()); var sencBox = FullBox("senc", 1, 0, stream.ToArray());
var moofBox = Box("moof", sencBox); //Movie Extends Box var moofBox = Box("moof", sencBox); // Movie Extends Box
return moofBox; return moofBox;
} }
@ -805,7 +784,7 @@ namespace N_m3u8DL_RE.Parser.Mp4
new MP4Parser() new MP4Parser()
.Box("moof", MP4Parser.Children) .Box("moof", MP4Parser.Children)
.Box("traf", MP4Parser.Children) .Box("traf", MP4Parser.Children)
.FullBox("tfhd", (box) => .FullBox("tfhd", box =>
{ {
TrackId = (int)box.Reader.ReadUInt32(); TrackId = (int)box.Reader.ReadUInt32();
}) })
@ -834,41 +813,41 @@ namespace N_m3u8DL_RE.Parser.Mp4
var minfPayload = GenMinf(); var minfPayload = GenMinf();
var sttsPayload = new byte[] { 0, 0, 0, 0 }; //entry count var sttsPayload = new byte[] { 0, 0, 0, 0 }; // entry count
var stblPayload = FullBox("stts", 0, 0, sttsPayload); //Decoding Time to Sample Box var stblPayload = FullBox("stts", 0, 0, sttsPayload); // Decoding Time to Sample Box
var stscPayload = new byte[] { 0, 0, 0, 0 }; //entry count var stscPayload = new byte[] { 0, 0, 0, 0 }; // entry count
var stscBox = FullBox("stsc", 0, 0, stscPayload); //Sample To Chunk Box var stscBox = FullBox("stsc", 0, 0, stscPayload); // Sample To Chunk Box
var stcoPayload = new byte[] { 0, 0, 0, 0 }; //entry count var stcoPayload = new byte[] { 0, 0, 0, 0 }; // entry count
var stcoBox = FullBox("stco", 0, 0, stcoPayload); //Chunk Offset Box var stcoBox = FullBox("stco", 0, 0, stcoPayload); // Chunk Offset Box
var stszPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; //sample size, sample count var stszPayload = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; // sample size, sample count
var stszBox = FullBox("stsz", 0, 0, stszPayload); //Sample Size Box var stszBox = FullBox("stsz", 0, 0, stszPayload); // Sample Size Box
var stsdPayload = GetStsd(); var stsdPayload = GetStsd();
var stsdBox = FullBox("stsd", 0, 0, stsdPayload); //Sample Description Box var stsdBox = FullBox("stsd", 0, 0, stsdPayload); // Sample Description Box
stblPayload = stblPayload.Concat(stscBox).Concat(stcoBox).Concat(stszBox).Concat(stsdBox).ToArray(); stblPayload = stblPayload.Concat(stscBox).Concat(stcoBox).Concat(stszBox).Concat(stsdBox).ToArray();
var stblBox = Box("stbl", stblPayload); //Sample Table Box var stblBox = Box("stbl", stblPayload); // Sample Table Box
minfPayload = minfPayload.Concat(stblBox).ToArray(); minfPayload = minfPayload.Concat(stblBox).ToArray();
var minfBox = Box("minf", minfPayload); //Media Information Box var minfBox = Box("minf", minfPayload); // Media Information Box
mdiaPayload = mdiaPayload.Concat(minfBox).ToArray(); mdiaPayload = mdiaPayload.Concat(minfBox).ToArray();
var mdiaBox = Box("mdia", mdiaPayload); //Media Box var mdiaBox = Box("mdia", mdiaPayload); // Media Box
trakPayload = trakPayload.Concat(mdiaBox).ToArray(); trakPayload = trakPayload.Concat(mdiaBox).ToArray();
var trakBox = Box("trak", trakPayload); //Track Box var trakBox = Box("trak", trakPayload); // Track Box
moovPayload = moovPayload.Concat(trakBox).ToArray(); moovPayload = moovPayload.Concat(trakBox).ToArray();
var mvexPayload = GetMehd(); var mvexPayload = GetMehd();
var trexBox = GetTrex(); var trexBox = GetTrex();
mvexPayload = mvexPayload.Concat(trexBox).ToArray(); mvexPayload = mvexPayload.Concat(trexBox).ToArray();
var mvexBox = Box("mvex", mvexPayload); //Movie Extends Box var mvexBox = Box("mvex", mvexPayload); // Movie Extends Box
moovPayload = moovPayload.Concat(mvexBox).ToArray(); moovPayload = moovPayload.Concat(mvexBox).ToArray();
if (IsProtection) if (IsProtection)
@ -878,14 +857,13 @@ namespace N_m3u8DL_RE.Parser.Mp4
moovPayload = moovPayload.Concat(psshBox1).Concat(psshBox2).ToArray(); moovPayload = moovPayload.Concat(psshBox1).Concat(psshBox2).ToArray();
} }
var moovBox = Box("moov", moovPayload); //Movie Box var moovBox = Box("moov", moovPayload); // Movie Box
stream.Write(moovBox); stream.Write(moovBox);
//var moofBox = GenMoof(); //Movie Extends Box // var moofBox = GenMoof(); // Movie Extends Box
//stream.Write(moofBox); // stream.Write(moofBox);
return stream.ToArray(); return stream.ToArray();
} }
}
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>library</OutputType> <OutputType>library</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RootNamespace>N_m3u8DL_RE.Parser</RootNamespace> <RootNamespace>N_m3u8DL_RE.Parser</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>

View File

@ -1,16 +1,10 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public abstract class ContentProcessor
{ {
public abstract class ContentProcessor
{
public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig); public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig);
public abstract string Process(string rawText, ParserConfig parserConfig); public abstract string Process(string rawText, ParserConfig parserConfig);
}
} }

View File

@ -1,28 +1,19 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.DASH namespace N_m3u8DL_RE.Parser.Processor.DASH;
/// <summary>
/// XG视频处理
/// </summary>
public class DefaultDASHContentProcessor : ContentProcessor
{ {
/// <summary>
/// 西瓜视频处理
/// </summary>
public class DefaultDASHContentProcessor : ContentProcessor
{
public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig) public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig)
{ {
if (extractorType != ExtractorType.MPEG_DASH) return false; if (extractorType != ExtractorType.MPEG_DASH) return false;
if (mpdContent.Contains("<mas:") && !mpdContent.Contains("xmlns:mas")) return mpdContent.Contains("<mas:") && !mpdContent.Contains("xmlns:mas");
{
return true;
}
return false;
} }
public override string Process(string mpdContent, ParserConfig parserConfig) public override string Process(string mpdContent, ParserConfig parserConfig)
@ -32,5 +23,4 @@ namespace N_m3u8DL_RE.Parser.Processor.DASH
return mpdContent; return mpdContent;
} }
}
} }

View File

@ -1,24 +1,18 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web; using System.Web;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public class DefaultUrlProcessor : UrlProcessor
{ {
public class DefaultUrlProcessor : UrlProcessor
{
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams; public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams;
public override string Process(string oriUrl, ParserConfig paserConfig) public override string Process(string oriUrl, ParserConfig paserConfig)
{ {
if (oriUrl.StartsWith("http")) if (!oriUrl.StartsWith("http")) return oriUrl;
{
var uriFromConfig = new Uri(paserConfig.Url); var uriFromConfig = new Uri(paserConfig.Url);
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query); var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
@ -32,15 +26,12 @@ namespace N_m3u8DL_RE.Parser.Processor
newQuery.Add(item, uriFromConfigQuery.Get(item)); newQuery.Add(item, uriFromConfigQuery.Get(item));
} }
if (!string.IsNullOrEmpty(newQuery.ToString())) if (string.IsNullOrEmpty(newQuery.ToString())) return oriUrl;
{
Logger.Debug("Before: " + oriUrl); Logger.Debug("Before: " + oriUrl);
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery.ToString()).TrimEnd('?'); oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery).TrimEnd('?');
Logger.Debug("After: " + oriUrl); Logger.Debug("After: " + oriUrl);
}
}
return oriUrl; return oriUrl;
} }
}
} }

View File

@ -1,70 +1,59 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS namespace N_m3u8DL_RE.Parser.Processor.HLS;
public partial class DefaultHLSContentProcessor : ContentProcessor
{ {
public partial class DefaultHLSContentProcessor : ContentProcessor
{
[GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")] [GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")]
private static partial Regex YkDVRegex(); private static partial Regex YkDVRegex();
[GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")] [GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPRegex(); private static partial Regex DNSPRegex();
[GeneratedRegex("#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY")] [GeneratedRegex(@"#EXTINF:.*?,\s+.*BUMPER.*\s+?#EXT-X-DISCONTINUITY")]
private static partial Regex DNSPSubRegex(); private static partial Regex DNSPSubRegex();
[GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")] [GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
private static partial Regex OrderFixRegex(); private static partial Regex OrderFixRegex();
[GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")] [GeneratedRegex(@"#EXT-X-MAP.*\.apple\.com/")]
private static partial Regex ATVRegex(); private static partial Regex ATVRegex();
[GeneratedRegex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")] [GeneratedRegex(@"(#EXT-X-KEY:[\s\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
private static partial Regex ATVRegex2(); private static partial Regex ATVRegex2();
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS; public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
public override string Process(string m3u8Content, ParserConfig parserConfig) public override string Process(string m3u8Content, ParserConfig parserConfig)
{ {
//处理content以\r作为换行符的情况 // 处理content以\r作为换行符的情况
if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n")) if (m3u8Content.Contains('\r') && !m3u8Content.Contains('\n'))
{ {
m3u8Content = m3u8Content.Replace("\r", Environment.NewLine); m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
} }
var m3u8Url = parserConfig.Url; var m3u8Url = parserConfig.Url;
//央视频回放 // YSP回放
if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime=")) if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime="))
{ {
m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist; m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
} }
//IMOOC // IMOOC
if (m3u8Url.Contains("imooc.com/")) if (m3u8Url.Contains("imooc.com/"))
{ {
//M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content); // M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
} }
//iqy // 针对YK #EXT-X-VERSION:7杜比视界片源修正
if (m3u8Content.StartsWith("{\"payload\""))
{
//
}
//针对优酷#EXT-X-VERSION:7杜比视界片源修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode=")) if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
{ {
Regex ykmap = YkDVRegex(); var ykmap = YkDVRegex();
foreach (Match m in ykmap.Matches(m3u8Content)) foreach (Match m in ykmap.Matches(m3u8Content))
{ {
m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}"); m3u8Content = m3u8Content.Replace(m.Value, $"#EXTINF:0.000000,\n#EXT-X-BYTERANGE:{m.Groups[2].Value}\n{m.Groups[1].Value}");
} }
} }
//针对Disney+修正 // 针对Disney+修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/")) if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
{ {
Regex ykmap = DNSPRegex(); Regex ykmap = DNSPRegex();
@ -74,7 +63,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
} }
} }
//针对Disney+字幕修正 // 针对Disney+字幕修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/")) if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/"))
{ {
Regex ykmap = DNSPSubRegex(); Regex ykmap = DNSPSubRegex();
@ -84,10 +73,10 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
} }
} }
//针对AppleTv修正 // 针对AppleTv修正
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || ATVRegex().IsMatch(m3u8Content))) if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || ATVRegex().IsMatch(m3u8Content)))
{ {
//只取加密部分即可 // 只取加密部分即可
Regex ykmap = ATVRegex2(); Regex ykmap = ATVRegex2();
if (ykmap.IsMatch(m3u8Content)) if (ykmap.IsMatch(m3u8Content))
{ {
@ -95,7 +84,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
} }
} }
//修复#EXT-X-KEY与#EXTINF出现次序异常问题 // 修复#EXT-X-KEY与#EXTINF出现次序异常问题
var regex = OrderFixRegex(); var regex = OrderFixRegex();
if (regex.IsMatch(m3u8Content)) if (regex.IsMatch(m3u8Content))
{ {
@ -104,5 +93,4 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
return m3u8Content; return m3u8Content;
} }
}
} }

View File

@ -6,16 +6,11 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Util; using N_m3u8DL_RE.Parser.Util;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor.HLS namespace N_m3u8DL_RE.Parser.Processor.HLS;
public class DefaultHLSKeyProcessor : KeyProcessor
{ {
public class DefaultHLSKeyProcessor : KeyProcessor
{
public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS; public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS;
@ -29,21 +24,21 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
var encryptInfo = new EncryptInfo(method); var encryptInfo = new EncryptInfo(method);
//IV // IV
if (!string.IsNullOrEmpty(iv)) if (!string.IsNullOrEmpty(iv))
{ {
encryptInfo.IV = HexUtil.HexToBytes(iv); encryptInfo.IV = HexUtil.HexToBytes(iv);
} }
//自定义IV // 自定义IV
if (parserConfig.CustomeIV != null && parserConfig.CustomeIV.Length > 0) if (parserConfig.CustomeIV is { Length: > 0 })
{ {
encryptInfo.IV = parserConfig.CustomeIV; encryptInfo.IV = parserConfig.CustomeIV;
} }
//KEY // KEY
try try
{ {
if (parserConfig.CustomeKey != null && parserConfig.CustomeKey.Length > 0) if (parserConfig.CustomeKey is { Length: > 0 })
{ {
encryptInfo.Key = parserConfig.CustomeKey; encryptInfo.Key = parserConfig.CustomeKey;
} }
@ -78,7 +73,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]"); Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
Thread.Sleep(1000); Thread.Sleep(1000);
if (retryCount-- > 0) goto getHttpKey; if (retryCount-- > 0) goto getHttpKey;
else throw; throw;
} }
} }
} }
@ -88,12 +83,11 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
encryptInfo.Method = EncryptMethod.UNKNOWN; encryptInfo.Method = EncryptMethod.UNKNOWN;
} }
//处理自定义加密方式 if (parserConfig.CustomMethod == null) return encryptInfo;
if (parserConfig.CustomMethod != null)
{ // 处理自定义加密方式
encryptInfo.Method = parserConfig.CustomMethod.Value; encryptInfo.Method = parserConfig.CustomMethod.Value;
Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method); Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
}
return encryptInfo; return encryptInfo;
} }
@ -113,5 +107,4 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS
return url; return url;
} }
}
} }

View File

@ -1,17 +1,11 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public abstract class KeyProcessor
{ {
public abstract class KeyProcessor
{
public abstract bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig); public abstract bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
public abstract EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig); public abstract EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig);
}
} }

View File

@ -1,16 +1,10 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Processor namespace N_m3u8DL_RE.Parser.Processor;
public abstract class UrlProcessor
{ {
public abstract class UrlProcessor
{
public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig); public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig);
public abstract string Process(string oriUrl, ParserConfig parserConfig); public abstract string Process(string oriUrl, ParserConfig parserConfig);
}
} }

View File

@ -1,4 +1,5 @@
using N_m3u8DL_RE.Parser.Config; using System.Diagnostics.CodeAnalysis;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
@ -6,24 +7,18 @@ using N_m3u8DL_RE.Parser.Constants;
using N_m3u8DL_RE.Parser.Extractor; using N_m3u8DL_RE.Parser.Extractor;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using Spectre.Console;
namespace N_m3u8DL_RE.Parser namespace N_m3u8DL_RE.Parser;
public class StreamExtractor
{ {
public class StreamExtractor public ExtractorType ExtractorType => extractor.ExtractorType;
{
public ExtractorType ExtractorType { get => extractor.ExtractorType; }
private IExtractor extractor; private IExtractor extractor;
private ParserConfig parserConfig = new ParserConfig(); private ParserConfig parserConfig = new();
private string rawText; private string rawText;
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); private static SemaphoreSlim semaphore = new(1, 1);
public Dictionary<string, string> RawFiles { get; set; } = new(); //存储(文件名,文件内容) public Dictionary<string, string> RawFiles { get; set; } = new(); // 存储(文件名,文件内容)
public StreamExtractor()
{
}
public StreamExtractor(ParserConfig parserConfig) public StreamExtractor(ParserConfig parserConfig)
{ {
@ -55,7 +50,8 @@ namespace N_m3u8DL_RE.Parser
LoadSourceFromText(this.rawText); LoadSourceFromText(this.rawText);
} }
public void LoadSourceFromText(string rawText) [MemberNotNull(nameof(this.rawText), nameof(this.extractor))]
private void LoadSourceFromText(string rawText)
{ {
var rawType = "txt"; var rawType = "txt";
rawText = rawText.Trim(); rawText = rawText.Trim();
@ -69,14 +65,14 @@ namespace N_m3u8DL_RE.Parser
else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD")) else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD"))
{ {
Logger.InfoMarkUp(ResString.matchDASH); Logger.InfoMarkUp(ResString.matchDASH);
//extractor = new DASHExtractor(parserConfig); // extractor = new DASHExtractor(parserConfig);
extractor = new DASHExtractor2(parserConfig); extractor = new DASHExtractor2(parserConfig);
rawType = "mpd"; rawType = "mpd";
} }
else if (rawText.Contains("</SmoothStreamingMedia>") && rawText.Contains("<SmoothStreamingMedia")) else if (rawText.Contains("</SmoothStreamingMedia>") && rawText.Contains("<SmoothStreamingMedia"))
{ {
Logger.InfoMarkUp(ResString.matchMSS); Logger.InfoMarkUp(ResString.matchMSS);
//extractor = new DASHExtractor(parserConfig); // extractor = new DASHExtractor(parserConfig);
extractor = new MSSExtractor(parserConfig); extractor = new MSSExtractor(parserConfig);
rawType = "ism"; rawType = "ism";
} }
@ -145,5 +141,4 @@ namespace N_m3u8DL_RE.Parser
semaphore.Release(); semaphore.Release();
} }
} }
}
} }

View File

@ -1,16 +1,11 @@
using N_m3u8DL_RE.Parser.Constants; using N_m3u8DL_RE.Parser.Constants;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Parser.Util namespace N_m3u8DL_RE.Parser.Util;
public static partial class ParserUtil
{ {
public partial class ParserUtil [GeneratedRegex(@"\$Number%([^$]+)d\$")]
{
[GeneratedRegex("\\$Number%([^$]+)d\\$")]
private static partial Regex VarsNumberRegex(); private static partial Regex VarsNumberRegex();
/// <summary> /// <summary>
@ -28,18 +23,17 @@ namespace N_m3u8DL_RE.Parser.Util
var index = -1; var index = -1;
var result = string.Empty; var result = string.Empty;
if ((index = line.IndexOf(key + "=\"")) > -1) if ((index = line.IndexOf(key + "=\"", StringComparison.Ordinal)) > -1)
{ {
var startIndex = index + (key + "=\"").Length; var startIndex = index + (key + "=\"").Length;
var endIndex = startIndex + line[startIndex..].IndexOf('\"'); var endIndex = startIndex + line[startIndex..].IndexOf('\"');
result = line[startIndex..endIndex]; result = line[startIndex..endIndex];
} }
else if ((index = line.IndexOf(key + "=")) > -1) else if ((index = line.IndexOf(key + "=", StringComparison.Ordinal)) > -1)
{ {
var startIndex = index + (key + "=").Length; var startIndex = index + (key + "=").Length;
var endIndex = startIndex + line[startIndex..].IndexOf(','); var endIndex = startIndex + line[startIndex..].IndexOf(',');
if (endIndex >= startIndex) result = line[startIndex..endIndex]; result = endIndex >= startIndex ? line[startIndex..endIndex] : line[startIndex..];
else result = line[startIndex..];
} }
return result; return result;
@ -54,18 +48,13 @@ namespace N_m3u8DL_RE.Parser.Util
public static (long, long?) GetRange(string input) public static (long, long?) GetRange(string input)
{ {
var t = input.Split('@'); var t = input.Split('@');
if (t.Length > 0) return t.Length switch
{ {
if (t.Length == 1) <= 0 => (0, null),
{ 1 => (Convert.ToInt64(t[0]), null),
return (Convert.ToInt64(t[0]), null); 2 => (Convert.ToInt64(t[0]), Convert.ToInt64(t[1])),
} _ => (0, null)
if (t.Length == 2) };
{
return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
}
}
return (0, null);
} }
/// <summary> /// <summary>
@ -92,13 +81,13 @@ namespace N_m3u8DL_RE.Parser.Util
if (text.Contains(item.Key)) if (text.Contains(item.Key))
text = text.Replace(item.Key, item.Value!.ToString()); text = text.Replace(item.Key, item.Value!.ToString());
//处理特殊形式数字 如 $Number%05d$ // 处理特殊形式数字 如 $Number%05d$
var regex = VarsNumberRegex(); var regex = VarsNumberRegex();
if (regex.IsMatch(text) && keyValuePairs.ContainsKey(DASHTags.TemplateNumber)) if (regex.IsMatch(text) && keyValuePairs.TryGetValue(DASHTags.TemplateNumber, out var keyValuePair))
{ {
foreach (Match m in regex.Matches(text)) foreach (Match m in regex.Matches(text))
{ {
text = text.Replace(m.Value, keyValuePairs[DASHTags.TemplateNumber]?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0')); text = text.Replace(m.Value, keyValuePair?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));
} }
} }
@ -116,11 +105,10 @@ namespace N_m3u8DL_RE.Parser.Util
if (string.IsNullOrEmpty(baseurl)) if (string.IsNullOrEmpty(baseurl))
return url; return url;
Uri uri1 = new Uri(baseurl); //这里直接传完整的URL即可 var uri1 = new Uri(baseurl); // 这里直接传完整的URL即可
Uri uri2 = new Uri(uri1, url); var uri2 = new Uri(uri1, url);
url = uri2.ToString(); url = uri2.ToString();
return url; return url;
} }
}
} }

View File

@ -1,18 +1,13 @@
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using N_m3u8DL_RE.Common.Util;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal sealed class DownloadSpeedColumn : ProgressColumn
{ {
internal sealed class DownloadSpeedColumn : ProgressColumn
{
private long _stopSpeed = 0; private long _stopSpeed = 0;
private ConcurrentDictionary<int, string> DateTimeStringDic = new(); private ConcurrentDictionary<int, string> DateTimeStringDic = new();
protected override bool NoWrap => true; protected override bool NoWrap => true;
@ -31,36 +26,23 @@ namespace N_m3u8DL_RE.Column
var speedContainer = SpeedContainerDic[taskId]; var speedContainer = SpeedContainerDic[taskId];
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var flag = task.IsFinished || !task.IsStarted; var flag = task.IsFinished || !task.IsStarted;
//单文件下载汇报进度 // 单文件下载汇报进度
if (!flag && speedContainer.SingleSegment && speedContainer.ResponseLength != null) if (!flag && speedContainer is { SingleSegment: true, ResponseLength: not null })
{ {
task.MaxValue = (double)speedContainer.ResponseLength; task.MaxValue = (double)speedContainer.ResponseLength;
task.Value = speedContainer.RDownloaded; task.Value = speedContainer.RDownloaded;
} }
//一秒汇报一次即可 // 一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag) if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
{ {
speedContainer.NowSpeed = speedContainer.Downloaded; speedContainer.NowSpeed = speedContainer.Downloaded;
//速度为0计数增加 // 速度为0计数增加
if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); } if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
else speedContainer.ResetLowSpeedCount(); else speedContainer.ResetLowSpeedCount();
speedContainer.Reset(); speedContainer.Reset();
} }
DateTimeStringDic[taskId] = now; DateTimeStringDic[taskId] = now;
var style = flag ? Style.Plain : MyStyle; var style = flag ? Style.Plain : MyStyle;
return flag ? new Text("-", style).Centered() : new Text(FormatFileSize(speedContainer.NowSpeed) + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered(); return flag ? new Text("-", style).Centered() : new Text(GlobalUtil.FormatFileSize(speedContainer.NowSpeed) + "ps" + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered();
}
private static string FormatFileSize(double fileSize)
{
return fileSize switch
{
< 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),
>= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GBps", (double)fileSize / (1024 * 1024 * 1024)),
>= 1024 * 1024 => string.Format("{0:####0.00}MBps", (double)fileSize / (1024 * 1024)),
>= 1024 => string.Format("{0:####0.00}KBps", (double)fileSize / 1024),
_ => string.Format("{0:####0.00}Bps", fileSize)
};
}
} }
} }

View File

@ -2,17 +2,12 @@
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class DownloadStatusColumn : ProgressColumn
{ {
internal class DownloadStatusColumn : ProgressColumn
{
private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; } private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
private ConcurrentDictionary<int, string> DateTimeStringDic = new(); private ConcurrentDictionary<int, string> DateTimeStringDic = new();
private ConcurrentDictionary<int, string> SizeDic = new(); private ConcurrentDictionary<int, string> SizeDic = new();
@ -32,7 +27,7 @@ namespace N_m3u8DL_RE.Column
var speedContainer = SpeedContainerDic[task.Id]; var speedContainer = SpeedContainerDic[task.Id];
var size = speedContainer.RDownloaded; var size = speedContainer.RDownloaded;
//一秒汇报一次即可 // 一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now) if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
{ {
var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue)); var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
@ -45,5 +40,4 @@ namespace N_m3u8DL_RE.Column
return new Text(sizeStr ?? "-", MyStyle).RightJustified(); return new Text(sizeStr ?? "-", MyStyle).RightJustified();
} }
}
} }

View File

@ -1,15 +1,10 @@
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class MyPercentageColumn : ProgressColumn
{ {
internal class MyPercentageColumn : ProgressColumn
{
/// <summary> /// <summary>
/// Gets or sets the style for a non-complete task. /// Gets or sets the style for a non-complete task.
/// </summary> /// </summary>
@ -27,5 +22,4 @@ namespace N_m3u8DL_RE.Column
var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain; var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified(); return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
} }
}
} }

View File

@ -1,17 +1,12 @@
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class RecordingDurationColumn : ProgressColumn
{ {
internal class RecordingDurationColumn : ProgressColumn
{
protected override bool NoWrap => true; protected override bool NoWrap => true;
private ConcurrentDictionary<int, int> _recodingDurDic; private ConcurrentDictionary<int, int> _recodingDurDic;
private ConcurrentDictionary<int, int>? _refreshedDurDic; private ConcurrentDictionary<int, int>? _refreshedDurDic;
@ -30,10 +25,6 @@ namespace N_m3u8DL_RE.Column
{ {
if (_refreshedDurDic == null) if (_refreshedDurDic == null)
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified(); return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
else
{
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle); return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
} }
}
}
} }

View File

@ -1,20 +1,14 @@
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class RecordingSizeColumn : ProgressColumn
{ {
internal class RecordingSizeColumn : ProgressColumn
{
protected override bool NoWrap => true; protected override bool NoWrap => true;
private ConcurrentDictionary<int, double> RecodingSizeDic = new(); //临时的大小 每秒刷新用 private ConcurrentDictionary<int, double> RecodingSizeDic = new(); // 临时的大小 每秒刷新用
private ConcurrentDictionary<int, double> _recodingSizeDic; private ConcurrentDictionary<int, double> _recodingSizeDic;
private ConcurrentDictionary<int, string> DateTimeStringDic = new(); private ConcurrentDictionary<int, string> DateTimeStringDic = new();
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan); public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
@ -26,7 +20,7 @@ namespace N_m3u8DL_RE.Column
{ {
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var taskId = task.Id; var taskId = task.Id;
//一秒汇报一次即可 // 一秒汇报一次即可
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now) if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
{ {
RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id]; RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
@ -35,5 +29,4 @@ namespace N_m3u8DL_RE.Column
var flag = RecodingSizeDic.TryGetValue(taskId, out var size); var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified(); return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
} }
}
} }

View File

@ -1,15 +1,10 @@
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Column namespace N_m3u8DL_RE.Column;
internal class RecordingStatusColumn : ProgressColumn
{ {
internal class RecordingStatusColumn : ProgressColumn
{
protected override bool NoWrap => true; protected override bool NoWrap => true;
public Style MyStyle { get; set; } = new Style(foreground: Color.Default); public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow); public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
@ -19,5 +14,4 @@ namespace N_m3u8DL_RE.Column
return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified(); return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified();
return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified(); return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
} }
}
} }

View File

@ -5,7 +5,6 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Enum; using N_m3u8DL_RE.Enum;
using N_m3u8DL_RE.Util; using N_m3u8DL_RE.Util;
using NiL.JS.Expressions;
using System.CommandLine; using System.CommandLine;
using System.CommandLine.Binding; using System.CommandLine.Binding;
using System.CommandLine.Builder; using System.CommandLine.Builder;
@ -14,115 +13,120 @@ using System.Globalization;
using System.Net; using System.Net;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.CommandLine namespace N_m3u8DL_RE.CommandLine;
internal static partial class CommandInvoker
{ {
internal partial class CommandInvoker public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20241216";
{
public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20240630";
[GeneratedRegex("((best|worst)\\d*|all)")] [GeneratedRegex("((best|worst)\\d*|all)")]
private static partial Regex ForStrRegex(); private static partial Regex ForStrRegex();
[GeneratedRegex("(\\d*)-(\\d*)")] [GeneratedRegex(@"(\d*)-(\d*)")]
private static partial Regex RangeRegex(); private static partial Regex RangeRegex();
[GeneratedRegex(@"([\d\\.]+)(M|K)")]
private static partial Regex SpeedStrRegex();
private readonly static Argument<string> Input = new(name: "input", description: ResString.cmd_Input); private static readonly Argument<string> Input = new(name: "input", description: ResString.cmd_Input);
private readonly static Option<string?> TmpDir = new(new string[] { "--tmp-dir" }, description: ResString.cmd_tmpDir); private static readonly Option<string?> TmpDir = new(["--tmp-dir"], description: ResString.cmd_tmpDir);
private readonly static Option<string?> SaveDir = new(new string[] { "--save-dir" }, description: ResString.cmd_saveDir); private static readonly Option<string?> SaveDir = new(["--save-dir"], description: ResString.cmd_saveDir);
private readonly static Option<string?> SaveName = new(new string[] { "--save-name" }, description: ResString.cmd_saveName, parseArgument: ParseSaveName); private static readonly Option<string?> SaveName = new(["--save-name"], description: ResString.cmd_saveName, parseArgument: ParseSaveName);
private readonly static Option<string?> SavePattern = new(new string[] { "--save-pattern" }, description: ResString.cmd_savePattern, getDefaultValue: () => "<SaveName>_<Id>_<Codecs>_<Language>_<Ext>"); private static readonly Option<string?> SavePattern = new(["--save-pattern"], description: ResString.cmd_savePattern, getDefaultValue: () => "<SaveName>_<Id>_<Codecs>_<Language>_<Ext>");
private readonly static Option<string?> UILanguage = new Option<string?>(new string[] { "--ui-language" }, description: ResString.cmd_uiLanguage).FromAmong("en-US", "zh-CN", "zh-TW"); private static readonly Option<string?> UILanguage = new Option<string?>(["--ui-language"], description: ResString.cmd_uiLanguage).FromAmong("en-US", "zh-CN", "zh-TW");
private readonly static Option<string?> UrlProcessorArgs = new(new string[] { "--urlprocessor-args" }, description: ResString.cmd_urlProcessorArgs); private static readonly Option<string?> UrlProcessorArgs = new(["--urlprocessor-args"], description: ResString.cmd_urlProcessorArgs);
private readonly static Option<string[]?> Keys = new(new string[] { "--key" }, description: ResString.cmd_keys) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false }; private static readonly Option<string[]?> Keys = new(["--key"], description: ResString.cmd_keys) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false };
private readonly static Option<string> KeyTextFile = new(new string[] { "--key-text-file" }, description: ResString.cmd_keyText); private static readonly Option<string> KeyTextFile = new(["--key-text-file"], description: ResString.cmd_keyText);
private readonly static Option<Dictionary<string, string>> Headers = new(new string[] { "-H", "--header" }, description: ResString.cmd_header, parseArgument: ParseHeaders) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false }; private static readonly Option<Dictionary<string, string>> Headers = new(["-H", "--header"], description: ResString.cmd_header, parseArgument: ParseHeaders) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false };
private readonly static Option<LogLevel> LogLevel = new(name: "--log-level", description: ResString.cmd_logLevel, getDefaultValue: () => Common.Log.LogLevel.INFO); private static readonly Option<LogLevel> LogLevel = new(name: "--log-level", description: ResString.cmd_logLevel, getDefaultValue: () => Common.Log.LogLevel.INFO);
private readonly static Option<SubtitleFormat> SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.SRT); private static readonly Option<SubtitleFormat> SubtitleFormat = new(name: "--sub-format", description: ResString.cmd_subFormat, getDefaultValue: () => Enum.SubtitleFormat.SRT);
private readonly static Option<bool> AutoSelect = new(new string[] { "--auto-select" }, description: ResString.cmd_autoSelect, getDefaultValue: () => false); private static readonly Option<bool> DisableUpdateCheck = new(["--disable-update-check"], description: ResString.cmd_disableUpdateCheck, getDefaultValue: () => false);
private readonly static Option<bool> SubOnly = new(new string[] { "--sub-only" }, description: ResString.cmd_subOnly, getDefaultValue: () => false); private static readonly Option<bool> AutoSelect = new(["--auto-select"], description: ResString.cmd_autoSelect, getDefaultValue: () => false);
private readonly static Option<int> ThreadCount = new(new string[] { "--thread-count" }, description: ResString.cmd_threadCount, getDefaultValue: () => Environment.ProcessorCount) { ArgumentHelpName = "number" }; private static readonly Option<bool> SubOnly = new(["--sub-only"], description: ResString.cmd_subOnly, getDefaultValue: () => false);
private readonly static Option<int> DownloadRetryCount = new(new string[] { "--download-retry-count" }, description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3) { ArgumentHelpName = "number" }; private static readonly Option<int> ThreadCount = new(["--thread-count"], description: ResString.cmd_threadCount, getDefaultValue: () => Environment.ProcessorCount) { ArgumentHelpName = "number" };
private readonly static Option<bool> SkipMerge = new(new string[] { "--skip-merge" }, description: ResString.cmd_skipMerge, getDefaultValue: () => false); private static readonly Option<int> DownloadRetryCount = new(["--download-retry-count"], description: ResString.cmd_downloadRetryCount, getDefaultValue: () => 3) { ArgumentHelpName = "number" };
private readonly static Option<bool> SkipDownload = new(new string[] { "--skip-download" }, description: ResString.cmd_skipDownload, getDefaultValue: () => false); private static readonly Option<double> HttpRequestTimeout = new(["--http-request-timeout"], description: ResString.cmd_httpRequestTimeout, getDefaultValue: () => 100) { ArgumentHelpName = "seconds" };
private readonly static Option<bool> NoDateInfo = new(new string[] { "--no-date-info" }, description: ResString.cmd_noDateInfo, getDefaultValue: () => false); private static readonly Option<bool> SkipMerge = new(["--skip-merge"], description: ResString.cmd_skipMerge, getDefaultValue: () => false);
private readonly static Option<bool> BinaryMerge = new(new string[] { "--binary-merge" }, description: ResString.cmd_binaryMerge, getDefaultValue: () => false); private static readonly Option<bool> SkipDownload = new(["--skip-download"], description: ResString.cmd_skipDownload, getDefaultValue: () => false);
private readonly static Option<bool> UseFFmpegConcatDemuxer = new(new string[] { "--use-ffmpeg-concat-demuxer" }, description: ResString.cmd_useFFmpegConcatDemuxer, getDefaultValue: () => false); private static readonly Option<bool> NoDateInfo = new(["--no-date-info"], description: ResString.cmd_noDateInfo, getDefaultValue: () => false);
private readonly static Option<bool> DelAfterDone = new(new string[] { "--del-after-done" }, description: ResString.cmd_delAfterDone, getDefaultValue: () => true); private static readonly Option<bool> BinaryMerge = new(["--binary-merge"], description: ResString.cmd_binaryMerge, getDefaultValue: () => false);
private readonly static Option<bool> AutoSubtitleFix = new(new string[] { "--auto-subtitle-fix" }, description: ResString.cmd_subtitleFix, getDefaultValue: () => true); private static readonly Option<bool> UseFFmpegConcatDemuxer = new(["--use-ffmpeg-concat-demuxer"], description: ResString.cmd_useFFmpegConcatDemuxer, getDefaultValue: () => false);
private readonly static Option<bool> CheckSegmentsCount = new(new string[] { "--check-segments-count" }, description: ResString.cmd_checkSegmentsCount, getDefaultValue: () => true); private static readonly Option<bool> DelAfterDone = new(["--del-after-done"], description: ResString.cmd_delAfterDone, getDefaultValue: () => true);
private readonly static Option<bool> WriteMetaJson = new(new string[] { "--write-meta-json" }, description: ResString.cmd_writeMetaJson, getDefaultValue: () => true); private static readonly Option<bool> AutoSubtitleFix = new(["--auto-subtitle-fix"], description: ResString.cmd_subtitleFix, getDefaultValue: () => true);
private readonly static Option<bool> AppendUrlParams = new(new string[] { "--append-url-params" }, description: ResString.cmd_appendUrlParams, getDefaultValue: () => false); private static readonly Option<bool> CheckSegmentsCount = new(["--check-segments-count"], description: ResString.cmd_checkSegmentsCount, getDefaultValue: () => true);
private readonly static Option<bool> MP4RealTimeDecryption = new (new string[] { "--mp4-real-time-decryption" }, description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false); private static readonly Option<bool> WriteMetaJson = new(["--write-meta-json"], description: ResString.cmd_writeMetaJson, getDefaultValue: () => true);
private readonly static Option<bool> UseShakaPackager = new (new string[] { "--use-shaka-packager" }, description: ResString.cmd_useShakaPackager, getDefaultValue: () => false); private static readonly Option<bool> AppendUrlParams = new(["--append-url-params"], description: ResString.cmd_appendUrlParams, getDefaultValue: () => false);
private readonly static Option<bool> ForceAnsiConsole = new(new string[] { "--force-ansi-console" }, description: ResString.cmd_forceAnsiConsole); private static readonly Option<bool> MP4RealTimeDecryption = new (["--mp4-real-time-decryption"], description: ResString.cmd_MP4RealTimeDecryption, getDefaultValue: () => false);
private readonly static Option<bool> NoAnsiColor = new(new string[] { "--no-ansi-color" }, description: ResString.cmd_noAnsiColor); private static readonly Option<bool> UseShakaPackager = new (["--use-shaka-packager"], description: ResString.cmd_useShakaPackager, getDefaultValue: () => false) { IsHidden = true };
private readonly static Option<string?> DecryptionBinaryPath = new(new string[] { "--decryption-binary-path" }, description: ResString.cmd_decryptionBinaryPath) { ArgumentHelpName = "PATH" }; private static readonly Option<DecryptEngine> DecryptionEngine = new (["--decryption-engine"], description: ResString.cmd_decryptionEngine, getDefaultValue: () => DecryptEngine.MP4DECRYPT);
private readonly static Option<string?> FFmpegBinaryPath = new(new string[] { "--ffmpeg-binary-path" }, description: ResString.cmd_ffmpegBinaryPath) { ArgumentHelpName = "PATH" }; private static readonly Option<bool> ForceAnsiConsole = new(["--force-ansi-console"], description: ResString.cmd_forceAnsiConsole);
private readonly static Option<string?> BaseUrl = new(new string[] { "--base-url" }, description: ResString.cmd_baseUrl); private static readonly Option<bool> NoAnsiColor = new(["--no-ansi-color"], description: ResString.cmd_noAnsiColor);
private readonly static Option<bool> ConcurrentDownload = new(new string[] { "-mt", "--concurrent-download" }, description: ResString.cmd_concurrentDownload, getDefaultValue: () => false); private static readonly Option<string?> DecryptionBinaryPath = new(["--decryption-binary-path"], description: ResString.cmd_decryptionBinaryPath) { ArgumentHelpName = "PATH" };
private readonly static Option<bool> NoLog = new(new string[] { "--no-log" }, description: ResString.cmd_noLog, getDefaultValue: () => false); private static readonly Option<string?> FFmpegBinaryPath = new(["--ffmpeg-binary-path"], description: ResString.cmd_ffmpegBinaryPath) { ArgumentHelpName = "PATH" };
private readonly static Option<string[]?> AdKeywords = new(new string[] { "--ad-keyword" }, description: ResString.cmd_adKeyword) { ArgumentHelpName = "REG" }; private static readonly Option<string?> BaseUrl = new(["--base-url"], description: ResString.cmd_baseUrl);
private readonly static Option<long?> MaxSpeed = new(new string[] { "-R", "--max-speed" }, description: ResString.cmd_maxSpeed, parseArgument: ParseSpeedLimit) { ArgumentHelpName = "SPEED" }; private static readonly Option<bool> ConcurrentDownload = new(["-mt", "--concurrent-download"], description: ResString.cmd_concurrentDownload, getDefaultValue: () => false);
private static readonly Option<bool> NoLog = new(["--no-log"], description: ResString.cmd_noLog, getDefaultValue: () => false);
private static readonly Option<bool> AllowHlsMultiExtMap = new(["--allow-hls-multi-ext-map"], description: ResString.cmd_allowHlsMultiExtMap, getDefaultValue: () => false);
private static readonly Option<string[]?> AdKeywords = new(["--ad-keyword"], description: ResString.cmd_adKeyword) { ArgumentHelpName = "REG" };
private static readonly Option<long?> MaxSpeed = new(["-R", "--max-speed"], description: ResString.cmd_maxSpeed, parseArgument: ParseSpeedLimit) { ArgumentHelpName = "SPEED" };
//代理选项 // 代理选项
private readonly static Option<bool> UseSystemProxy = new(new string[] { "--use-system-proxy" }, description: ResString.cmd_useSystemProxy, getDefaultValue: () => true); private static readonly Option<bool> UseSystemProxy = new(["--use-system-proxy"], description: ResString.cmd_useSystemProxy, getDefaultValue: () => true);
private readonly static Option<WebProxy?> CustomProxy = new(new string[] { "--custom-proxy" }, description: ResString.cmd_customProxy, parseArgument: ParseProxy) { ArgumentHelpName = "URL" }; private static readonly Option<WebProxy?> CustomProxy = new(["--custom-proxy"], description: ResString.cmd_customProxy, parseArgument: ParseProxy) { ArgumentHelpName = "URL" };
//只下载部分分片 // 只下载部分分片
private readonly static Option<CustomRange?> CustomRange = new(new string[] { "--custom-range" }, description: ResString.cmd_customRange, parseArgument: ParseCustomRange) { ArgumentHelpName = "RANGE" }; private static readonly Option<CustomRange?> CustomRange = new(["--custom-range"], description: ResString.cmd_customRange, parseArgument: ParseCustomRange) { ArgumentHelpName = "RANGE" };
//morehelp // morehelp
private readonly static Option<string?> MoreHelp = new(new string[] { "--morehelp" }, description: ResString.cmd_moreHelp) { ArgumentHelpName = "OPTION" }; private static readonly Option<string?> MoreHelp = new(["--morehelp"], description: ResString.cmd_moreHelp) { ArgumentHelpName = "OPTION" };
//自定义KEY等 // 自定义KEY等
private readonly static Option<EncryptMethod?> CustomHLSMethod = new(name: "--custom-hls-method", description: ResString.cmd_customHLSMethod) { ArgumentHelpName = "METHOD" }; private static readonly Option<EncryptMethod?> CustomHLSMethod = new(name: "--custom-hls-method", description: ResString.cmd_customHLSMethod) { ArgumentHelpName = "METHOD" };
private readonly static Option<byte[]?> CustomHLSKey = new(name: "--custom-hls-key", description: ResString.cmd_customHLSKey, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" }; private static readonly Option<byte[]?> CustomHLSKey = new(name: "--custom-hls-key", description: ResString.cmd_customHLSKey, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" };
private readonly static Option<byte[]?> CustomHLSIv = new(name: "--custom-hls-iv", description: ResString.cmd_customHLSIv, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" }; private static readonly Option<byte[]?> CustomHLSIv = new(name: "--custom-hls-iv", description: ResString.cmd_customHLSIv, parseArgument: ParseHLSCustomKey) { ArgumentHelpName = "FILE|HEX|BASE64" };
//任务开始时间 // 任务开始时间
private readonly static Option<DateTime?> TaskStartAt = new(new string[] { "--task-start-at" }, description: ResString.cmd_taskStartAt, parseArgument: ParseStartTime) { ArgumentHelpName = "yyyyMMddHHmmss" }; private static readonly Option<DateTime?> TaskStartAt = new(["--task-start-at"], description: ResString.cmd_taskStartAt, parseArgument: ParseStartTime) { ArgumentHelpName = "yyyyMMddHHmmss" };
//直播相关 // 直播相关
private readonly static Option<bool> LivePerformAsVod = new(new string[] { "--live-perform-as-vod" }, description: ResString.cmd_livePerformAsVod, getDefaultValue: () => false); private static readonly Option<bool> LivePerformAsVod = new(["--live-perform-as-vod"], description: ResString.cmd_livePerformAsVod, getDefaultValue: () => false);
private readonly static Option<bool> LiveRealTimeMerge = new(new string[] { "--live-real-time-merge" }, description: ResString.cmd_liveRealTimeMerge, getDefaultValue: () => false); private static readonly Option<bool> LiveRealTimeMerge = new(["--live-real-time-merge"], description: ResString.cmd_liveRealTimeMerge, getDefaultValue: () => false);
private readonly static Option<bool> LiveKeepSegments = new(new string[] { "--live-keep-segments" }, description: ResString.cmd_liveKeepSegments, getDefaultValue: () => true); private static readonly Option<bool> LiveKeepSegments = new(["--live-keep-segments"], description: ResString.cmd_liveKeepSegments, getDefaultValue: () => true);
private readonly static Option<bool> LivePipeMux = new(new string[] { "--live-pipe-mux" }, description: ResString.cmd_livePipeMux, getDefaultValue: () => false); private static readonly Option<bool> LivePipeMux = new(["--live-pipe-mux"], description: ResString.cmd_livePipeMux, getDefaultValue: () => false);
private readonly static Option<TimeSpan?> LiveRecordLimit = new(new string[] { "--live-record-limit" }, description: ResString.cmd_liveRecordLimit, parseArgument: ParseLiveLimit) { ArgumentHelpName = "HH:mm:ss" }; private static readonly Option<TimeSpan?> LiveRecordLimit = new(["--live-record-limit"], description: ResString.cmd_liveRecordLimit, parseArgument: ParseLiveLimit) { ArgumentHelpName = "HH:mm:ss" };
private readonly static Option<int?> LiveWaitTime = new(new string[] { "--live-wait-time" }, description: ResString.cmd_liveWaitTime) { ArgumentHelpName = "SEC" }; private static readonly Option<int?> LiveWaitTime = new(["--live-wait-time"], description: ResString.cmd_liveWaitTime) { ArgumentHelpName = "SEC" };
private readonly static Option<int> LiveTakeCount = new(new string[] { "--live-take-count" }, description: ResString.cmd_liveTakeCount, getDefaultValue: () => 16) { ArgumentHelpName = "NUM" }; private static readonly Option<int> LiveTakeCount = new(["--live-take-count"], description: ResString.cmd_liveTakeCount, getDefaultValue: () => 16) { ArgumentHelpName = "NUM" };
private readonly static Option<bool> LiveFixVttByAudio = new(new string[] { "--live-fix-vtt-by-audio" }, description: ResString.cmd_liveFixVttByAudio, getDefaultValue: () => false); private static readonly Option<bool> LiveFixVttByAudio = new(["--live-fix-vtt-by-audio"], description: ResString.cmd_liveFixVttByAudio, getDefaultValue: () => false);
//复杂命令行如下 // 复杂命令行如下
private readonly static Option<MuxOptions?> MuxAfterDone = new(new string[] { "-M", "--mux-after-done" }, description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" }; private static readonly Option<MuxOptions?> MuxAfterDone = new(["-M", "--mux-after-done"], description: ResString.cmd_muxAfterDone, parseArgument: ParseMuxAfterDone) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<List<OutputFile>> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" }; private static readonly Option<List<OutputFile>> MuxImports = new("--mux-import", description: ResString.cmd_muxImport, parseArgument: ParseImports) { Arity = ArgumentArity.OneOrMore, AllowMultipleArgumentsPerToken = false, ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> VideoFilter = new(new string[] { "-sv", "--select-video" }, description: ResString.cmd_selectVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private static readonly Option<StreamFilter?> VideoFilter = new(["-sv", "--select-video"], description: ResString.cmd_selectVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> AudioFilter = new(new string[] { "-sa", "--select-audio" }, description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private static readonly Option<StreamFilter?> AudioFilter = new(["-sa", "--select-audio"], description: ResString.cmd_selectAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> SubtitleFilter = new(new string[] { "-ss", "--select-subtitle" }, description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private static readonly Option<StreamFilter?> SubtitleFilter = new(["-ss", "--select-subtitle"], description: ResString.cmd_selectSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> DropVideoFilter = new(new string[] { "-dv", "--drop-video" }, description: ResString.cmd_dropVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private static readonly Option<StreamFilter?> DropVideoFilter = new(["-dv", "--drop-video"], description: ResString.cmd_dropVideo, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> DropAudioFilter = new(new string[] { "-da", "--drop-audio" }, description: ResString.cmd_dropAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private static readonly Option<StreamFilter?> DropAudioFilter = new(["-da", "--drop-audio"], description: ResString.cmd_dropAudio, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
private readonly static Option<StreamFilter?> DropSubtitleFilter = new(new string[] { "-ds", "--drop-subtitle" }, description: ResString.cmd_dropSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" }; private static readonly Option<StreamFilter?> DropSubtitleFilter = new(["-ds", "--drop-subtitle"], description: ResString.cmd_dropSubtitle, parseArgument: ParseStreamFilter) { ArgumentHelpName = "OPTIONS" };
/// <summary> /// <summary>
/// 解析录制直播时长限制 /// 解析下载速度限制
/// </summary> /// </summary>
/// <param name="result"></param> /// <param name="result"></param>
/// <returns></returns> /// <returns></returns>
private static long? ParseSpeedLimit(ArgumentResult result) private static long? ParseSpeedLimit(ArgumentResult result)
{ {
var input = result.Tokens.First().Value.ToUpper(); var input = result.Tokens[0].Value.ToUpper();
try try
{ {
var reg = new Regex("([\\d\\\\.]+)(M|K)"); var reg = SpeedStrRegex();
if (!reg.IsMatch(input)) throw new ArgumentException(); if (!reg.IsMatch(input)) throw new ArgumentException($"Invalid Speed Limit: {input}");
var number = double.Parse(reg.Match(input).Groups[1].Value); var number = double.Parse(reg.Match(input).Groups[1].Value);
if (reg.Match(input).Groups[2].Value == "M") if (reg.Match(input).Groups[2].Value == "M")
return (long)(number * 1024 * 1024); return (long)(number * 1024 * 1024);
else
return (long)(number * 1024); return (long)(number * 1024);
} }
catch (Exception) catch (Exception)
@ -140,8 +144,8 @@ namespace N_m3u8DL_RE.CommandLine
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
private static CustomRange? ParseCustomRange(ArgumentResult result) private static CustomRange? ParseCustomRange(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
//支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00; // 支持的种类 0-100; 01:00:00-02:30:00; -300; 300-; 05:00-; -03:00;
try try
{ {
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
@ -151,7 +155,7 @@ namespace N_m3u8DL_RE.CommandLine
if (arr.Length != 2) if (arr.Length != 2)
throw new ArgumentException("Bad format!"); throw new ArgumentException("Bad format!");
if (input.Contains(":")) if (input.Contains(':'))
{ {
return new CustomRange() return new CustomRange()
{ {
@ -160,7 +164,8 @@ namespace N_m3u8DL_RE.CommandLine
EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds, EndSec = arr[1] == "" ? double.MaxValue : OtherUtil.ParseDur(arr[1]).TotalSeconds,
}; };
} }
else if (RangeRegex().IsMatch(input))
if (RangeRegex().IsMatch(input))
{ {
var left = RangeRegex().Match(input).Groups[1].Value; var left = RangeRegex().Match(input).Groups[1].Value;
var right = RangeRegex().Match(input).Groups[2].Value; var right = RangeRegex().Match(input).Groups[2].Value;
@ -189,7 +194,7 @@ namespace N_m3u8DL_RE.CommandLine
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
private static WebProxy? ParseProxy(ArgumentResult result) private static WebProxy? ParseProxy(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
@ -218,16 +223,15 @@ namespace N_m3u8DL_RE.CommandLine
/// <returns></returns> /// <returns></returns>
private static byte[]? ParseHLSCustomKey(ArgumentResult result) private static byte[]? ParseHLSCustomKey(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
return null; return null;
if (File.Exists(input)) if (File.Exists(input))
return File.ReadAllBytes(input); return File.ReadAllBytes(input);
else if (HexUtil.TryParseHexString(input, out byte[]? bytes)) if (HexUtil.TryParseHexString(input, out byte[]? bytes))
return bytes; return bytes;
else
return Convert.FromBase64String(input); return Convert.FromBase64String(input);
} }
catch (Exception) catch (Exception)
@ -244,7 +248,7 @@ namespace N_m3u8DL_RE.CommandLine
/// <returns></returns> /// <returns></returns>
private static TimeSpan? ParseLiveLimit(ArgumentResult result) private static TimeSpan? ParseLiveLimit(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
return OtherUtil.ParseDur(input); return OtherUtil.ParseDur(input);
@ -263,7 +267,7 @@ namespace N_m3u8DL_RE.CommandLine
/// <returns></returns> /// <returns></returns>
private static DateTime? ParseStartTime(ArgumentResult result) private static DateTime? ParseStartTime(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
try try
{ {
CultureInfo provider = CultureInfo.InvariantCulture; CultureInfo provider = CultureInfo.InvariantCulture;
@ -278,7 +282,7 @@ namespace N_m3u8DL_RE.CommandLine
private static string? ParseSaveName(ArgumentResult result) private static string? ParseSaveName(ArgumentResult result)
{ {
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
var newName = OtherUtil.GetValidFileName(input); var newName = OtherUtil.GetValidFileName(input);
if (string.IsNullOrEmpty(newName)) if (string.IsNullOrEmpty(newName))
{ {
@ -296,11 +300,11 @@ namespace N_m3u8DL_RE.CommandLine
private static StreamFilter? ParseStreamFilter(ArgumentResult result) private static StreamFilter? ParseStreamFilter(ArgumentResult result)
{ {
var streamFilter = new StreamFilter(); var streamFilter = new StreamFilter();
var input = result.Tokens.First().Value; var input = result.Tokens[0].Value;
var p = new ComplexParamParser(input); var p = new ComplexParamParser(input);
//目标范围 // 目标范围
var forStr = ""; var forStr = "";
if (input == ForStrRegex().Match(input).Value) if (input == ForStrRegex().Match(input).Value)
{ {
@ -407,7 +411,7 @@ namespace N_m3u8DL_RE.CommandLine
foreach (var item in result.Tokens) foreach (var item in result.Tokens)
{ {
var p = new ComplexParamParser(item.Value); var p = new ComplexParamParser(item.Value);
var path = p.GetValue("path") ?? item.Value; //若未获取到直接整个字符串作为path var path = p.GetValue("path") ?? item.Value; // 若未获取到直接整个字符串作为path
var lang = p.GetValue("lang"); var lang = p.GetValue("lang");
var name = p.GetValue("name"); var name = p.GetValue("name");
if (string.IsNullOrEmpty(path) || !File.Exists(path)) if (string.IsNullOrEmpty(path) || !File.Exists(path))
@ -434,44 +438,45 @@ namespace N_m3u8DL_RE.CommandLine
/// <returns></returns> /// <returns></returns>
private static MuxOptions? ParseMuxAfterDone(ArgumentResult result) private static MuxOptions? ParseMuxAfterDone(ArgumentResult result)
{ {
var v = result.Tokens.First().Value; var v = result.Tokens[0].Value;
var p = new ComplexParamParser(v); var p = new ComplexParamParser(v);
//混流格式 // 混流格式
var format = p.GetValue("format") ?? v.Split(':')[0]; //若未获取到,直接:前的字符串作为format解析 var format = p.GetValue("format") ?? v.Split(':')[0]; // 若未获取到,直接:前的字符串作为format解析
if (format != "mp4" && format != "mkv") var parseResult = System.Enum.TryParse(format.ToUpperInvariant(), out MuxFormat muxFormat);
if (!parseResult)
{ {
result.ErrorMessage = $"format={format} not valid"; result.ErrorMessage = $"format={format} not valid";
return null; return null;
} }
//混流器 // 混流器
var muxer = p.GetValue("muxer") ?? "ffmpeg"; var muxer = p.GetValue("muxer") ?? "ffmpeg";
if (muxer != "ffmpeg" && muxer != "mkvmerge") if (muxer != "ffmpeg" && muxer != "mkvmerge")
{ {
result.ErrorMessage = $"muxer={muxer} not valid"; result.ErrorMessage = $"muxer={muxer} not valid";
return null; return null;
} }
//混流器路径 // 混流器路径
var bin_path = p.GetValue("bin_path") ?? "auto"; var bin_path = p.GetValue("bin_path") ?? "auto";
if (string.IsNullOrEmpty(bin_path)) if (string.IsNullOrEmpty(bin_path))
{ {
result.ErrorMessage = $"bin_path={bin_path} not valid"; result.ErrorMessage = $"bin_path={bin_path} not valid";
return null; return null;
} }
//是否删除 // 是否删除
var keep = p.GetValue("keep") ?? "false"; var keep = p.GetValue("keep") ?? "false";
if (keep != "true" && keep != "false") if (keep != "true" && keep != "false")
{ {
result.ErrorMessage = $"keep={keep} not valid"; result.ErrorMessage = $"keep={keep} not valid";
return null; return null;
} }
//是否忽略字幕 // 是否忽略字幕
var skipSub = p.GetValue("skip_sub") ?? "false"; var skipSub = p.GetValue("skip_sub") ?? "false";
if (skipSub != "true" && skipSub != "false") if (skipSub != "true" && skipSub != "false")
{ {
result.ErrorMessage = $"skip_sub={keep} not valid"; result.ErrorMessage = $"skip_sub={keep} not valid";
return null; return null;
} }
//冲突检测 // 冲突检测
if (muxer == "mkvmerge" && format == "mp4") if (muxer == "mkvmerge" && format == "mp4")
{ {
result.ErrorMessage = $"mkvmerge can not do mp4"; result.ErrorMessage = $"mkvmerge can not do mp4";
@ -480,7 +485,7 @@ namespace N_m3u8DL_RE.CommandLine
return new MuxOptions() return new MuxOptions()
{ {
UseMkvmerge = muxer == "mkvmerge", UseMkvmerge = muxer == "mkvmerge",
MuxToMp4 = format == "mp4", MuxFormat = muxFormat,
KeepFiles = keep == "true", KeepFiles = keep == "true",
SkipSubtitle = skipSub == "true", SkipSubtitle = skipSub == "true",
BinPath = bin_path == "auto" ? null : bin_path BinPath = bin_path == "auto" ? null : bin_path
@ -498,6 +503,7 @@ namespace N_m3u8DL_RE.CommandLine
NoAnsiColor = bindingContext.ParseResult.GetValueForOption(NoAnsiColor), NoAnsiColor = bindingContext.ParseResult.GetValueForOption(NoAnsiColor),
LogLevel = bindingContext.ParseResult.GetValueForOption(LogLevel), LogLevel = bindingContext.ParseResult.GetValueForOption(LogLevel),
AutoSelect = bindingContext.ParseResult.GetValueForOption(AutoSelect), AutoSelect = bindingContext.ParseResult.GetValueForOption(AutoSelect),
DisableUpdateCheck = bindingContext.ParseResult.GetValueForOption(DisableUpdateCheck),
SkipMerge = bindingContext.ParseResult.GetValueForOption(SkipMerge), SkipMerge = bindingContext.ParseResult.GetValueForOption(SkipMerge),
BinaryMerge = bindingContext.ParseResult.GetValueForOption(BinaryMerge), BinaryMerge = bindingContext.ParseResult.GetValueForOption(BinaryMerge),
UseFFmpegConcatDemuxer = bindingContext.ParseResult.GetValueForOption(UseFFmpegConcatDemuxer), UseFFmpegConcatDemuxer = bindingContext.ParseResult.GetValueForOption(UseFFmpegConcatDemuxer),
@ -519,10 +525,12 @@ namespace N_m3u8DL_RE.CommandLine
UrlProcessorArgs = bindingContext.ParseResult.GetValueForOption(UrlProcessorArgs), UrlProcessorArgs = bindingContext.ParseResult.GetValueForOption(UrlProcessorArgs),
MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption), MP4RealTimeDecryption = bindingContext.ParseResult.GetValueForOption(MP4RealTimeDecryption),
UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager), UseShakaPackager = bindingContext.ParseResult.GetValueForOption(UseShakaPackager),
DecryptionEngine = bindingContext.ParseResult.GetValueForOption(DecryptionEngine),
DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath), DecryptionBinaryPath = bindingContext.ParseResult.GetValueForOption(DecryptionBinaryPath),
FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath), FFmpegBinaryPath = bindingContext.ParseResult.GetValueForOption(FFmpegBinaryPath),
KeyTextFile = bindingContext.ParseResult.GetValueForOption(KeyTextFile), KeyTextFile = bindingContext.ParseResult.GetValueForOption(KeyTextFile),
DownloadRetryCount = bindingContext.ParseResult.GetValueForOption(DownloadRetryCount), DownloadRetryCount = bindingContext.ParseResult.GetValueForOption(DownloadRetryCount),
HttpRequestTimeout = bindingContext.ParseResult.GetValueForOption(HttpRequestTimeout),
BaseUrl = bindingContext.ParseResult.GetValueForOption(BaseUrl), BaseUrl = bindingContext.ParseResult.GetValueForOption(BaseUrl),
MuxImports = bindingContext.ParseResult.GetValueForOption(MuxImports), MuxImports = bindingContext.ParseResult.GetValueForOption(MuxImports),
ConcurrentDownload = bindingContext.ParseResult.GetValueForOption(ConcurrentDownload), ConcurrentDownload = bindingContext.ParseResult.GetValueForOption(ConcurrentDownload),
@ -546,6 +554,7 @@ namespace N_m3u8DL_RE.CommandLine
LiveTakeCount = bindingContext.ParseResult.GetValueForOption(LiveTakeCount), LiveTakeCount = bindingContext.ParseResult.GetValueForOption(LiveTakeCount),
NoDateInfo = bindingContext.ParseResult.GetValueForOption(NoDateInfo), NoDateInfo = bindingContext.ParseResult.GetValueForOption(NoDateInfo),
NoLog = bindingContext.ParseResult.GetValueForOption(NoLog), NoLog = bindingContext.ParseResult.GetValueForOption(NoLog),
AllowHlsMultiExtMap = bindingContext.ParseResult.GetValueForOption(AllowHlsMultiExtMap),
AdKeywords = bindingContext.ParseResult.GetValueForOption(AdKeywords), AdKeywords = bindingContext.ParseResult.GetValueForOption(AdKeywords),
MaxSpeed = bindingContext.ParseResult.GetValueForOption(MaxSpeed), MaxSpeed = bindingContext.ParseResult.GetValueForOption(MaxSpeed),
}; };
@ -559,7 +568,7 @@ namespace N_m3u8DL_RE.CommandLine
option.Headers = parsedHeaders; option.Headers = parsedHeaders;
//以用户选择语言为准优先 // 以用户选择语言为准优先
if (option.UILanguage != null) if (option.UILanguage != null)
{ {
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(option.UILanguage); CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(option.UILanguage);
@ -567,16 +576,14 @@ namespace N_m3u8DL_RE.CommandLine
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(option.UILanguage); Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(option.UILanguage);
} }
//混流设置 // 混流设置
var muxAfterDoneValue = bindingContext.ParseResult.GetValueForOption(MuxAfterDone); var muxAfterDoneValue = bindingContext.ParseResult.GetValueForOption(MuxAfterDone);
if (muxAfterDoneValue != null) if (muxAfterDoneValue == null) return option;
{
option.MuxAfterDone = true; option.MuxAfterDone = true;
option.MuxOptions = muxAfterDoneValue; option.MuxOptions = muxAfterDoneValue;
if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath; if (muxAfterDoneValue.UseMkvmerge) option.MkvmergeBinaryPath = muxAfterDoneValue.BinPath;
else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath; else option.FFmpegBinaryPath ??= muxAfterDoneValue.BinPath;
}
return option; return option;
} }
@ -606,19 +613,19 @@ namespace N_m3u8DL_RE.CommandLine
var rootCommand = new RootCommand(VERSION_INFO) var rootCommand = new RootCommand(VERSION_INFO)
{ {
Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount, Input, TmpDir, SaveDir, SaveName, BaseUrl, ThreadCount, DownloadRetryCount, HttpRequestTimeout, ForceAnsiConsole, NoAnsiColor,AutoSelect, SkipMerge, SkipDownload, CheckSegmentsCount,
BinaryMerge, UseFFmpegConcatDemuxer, DelAfterDone, NoDateInfo, NoLog, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix, BinaryMerge, UseFFmpegConcatDemuxer, DelAfterDone, NoDateInfo, NoLog, WriteMetaJson, AppendUrlParams, ConcurrentDownload, Headers, /**SavePattern,**/ SubOnly, SubtitleFormat, AutoSubtitleFix,
FFmpegBinaryPath, FFmpegBinaryPath,
LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionEngine, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption,
MaxSpeed, MaxSpeed,
MuxAfterDone, MuxAfterDone,
CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, CustomRange, TaskStartAt, CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, CustomRange, TaskStartAt,
LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveFixVttByAudio, LiveRecordLimit, LiveWaitTime, LiveTakeCount, LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveFixVttByAudio, LiveRecordLimit, LiveWaitTime, LiveTakeCount,
MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, AdKeywords, MoreHelp MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, AdKeywords, DisableUpdateCheck, AllowHlsMultiExtMap, MoreHelp
}; };
rootCommand.TreatUnmatchedTokensAsErrors = true; rootCommand.TreatUnmatchedTokensAsErrors = true;
rootCommand.SetHandler(async (myOption) => await action(myOption), new MyOptionBinder()); rootCommand.SetHandler(async myOption => await action(myOption), new MyOptionBinder());
var parser = new CommandLineBuilder(rootCommand) var parser = new CommandLineBuilder(rootCommand)
.UseDefaults() .UseDefaults()
@ -638,5 +645,4 @@ namespace N_m3u8DL_RE.CommandLine
return await parser.InvokeAsync(args); return await parser.InvokeAsync(args);
} }
}
} }

View File

@ -1,14 +1,10 @@
using System; using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.CommandLine namespace N_m3u8DL_RE.CommandLine;
internal class ComplexParamParser
{ {
internal class ComplexParamParser private readonly string _arg;
{
private string _arg;
public ComplexParamParser(string arg) public ComplexParamParser(string arg)
{ {
_arg = arg; _arg = arg;
@ -20,7 +16,7 @@ namespace N_m3u8DL_RE.CommandLine
try try
{ {
var index = _arg.IndexOf(key + "="); var index = _arg.IndexOf(key + "=", StringComparison.Ordinal);
if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null; if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;
var chars = _arg[(index + key.Length + 1)..].ToCharArray(); var chars = _arg[(index + key.Length + 1)..].ToCharArray();
@ -47,7 +43,7 @@ namespace N_m3u8DL_RE.CommandLine
var resultStr = result.ToString().Trim().Trim('\"').Trim('\''); var resultStr = result.ToString().Trim().Trim('\"').Trim('\'');
//不应该有引号出现 // 不应该有引号出现
if (resultStr.Contains('\"') || resultStr.Contains('\'')) throw new Exception(); if (resultStr.Contains('\"') || resultStr.Contains('\'')) throw new Exception();
return resultStr; return resultStr;
@ -57,5 +53,4 @@ namespace N_m3u8DL_RE.CommandLine
throw new ArgumentException($"Parse Argument [{key}] failed!"); throw new ArgumentException($"Parse Argument [{key}] failed!");
} }
} }
}
} }

View File

@ -4,10 +4,10 @@ using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Enum; using N_m3u8DL_RE.Enum;
using System.Net; using System.Net;
namespace N_m3u8DL_RE.CommandLine namespace N_m3u8DL_RE.CommandLine;
internal class MyOption
{ {
internal class MyOption
{
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.Input"/>. /// See: <see cref="CommandInvoker.Input"/>.
/// </summary> /// </summary>
@ -53,10 +53,18 @@ namespace N_m3u8DL_RE.CommandLine
/// </summary> /// </summary>
public bool NoLog { get; set; } public bool NoLog { get; set; }
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.AllowHlsMultiExtMap"/>.
/// </summary>
public bool AllowHlsMultiExtMap { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.AutoSelect"/>. /// See: <see cref="CommandInvoker.AutoSelect"/>.
/// </summary> /// </summary>
public bool AutoSelect { get; set; } public bool AutoSelect { get; set; }
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.DisableUpdateCheck"/>.
/// </summary>
public bool DisableUpdateCheck { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.SubOnly"/>. /// See: <see cref="CommandInvoker.SubOnly"/>.
/// </summary> /// </summary>
public bool SubOnly { get; set; } public bool SubOnly { get; set; }
@ -69,6 +77,10 @@ namespace N_m3u8DL_RE.CommandLine
/// </summary> /// </summary>
public int DownloadRetryCount { get; set; } public int DownloadRetryCount { get; set; }
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.HttpRequestTimeout"/>.
/// </summary>
public double HttpRequestTimeout { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.LiveRecordLimit"/>. /// See: <see cref="CommandInvoker.LiveRecordLimit"/>.
/// </summary> /// </summary>
public TimeSpan? LiveRecordLimit { get; set; } public TimeSpan? LiveRecordLimit { get; set; }
@ -127,8 +139,13 @@ namespace N_m3u8DL_RE.CommandLine
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.UseShakaPackager"/>. /// See: <see cref="CommandInvoker.UseShakaPackager"/>.
/// </summary> /// </summary>
[Obsolete("Use DecryptionEngine instead")]
public bool UseShakaPackager { get; set; } public bool UseShakaPackager { get; set; }
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.DecryptionEngine"/>.
/// </summary>
public DecryptEngine DecryptionEngine { get; set; }
/// <summary>
/// See: <see cref="CommandInvoker.MuxAfterDone"/>. /// See: <see cref="CommandInvoker.MuxAfterDone"/>.
/// </summary> /// </summary>
public bool MuxAfterDone { get; set; } public bool MuxAfterDone { get; set; }
@ -244,8 +261,8 @@ namespace N_m3u8DL_RE.CommandLine
/// See: <see cref="CommandInvoker.LiveTakeCount"/>. /// See: <see cref="CommandInvoker.LiveTakeCount"/>.
/// </summary> /// </summary>
public int LiveTakeCount { get; set; } public int LiveTakeCount { get; set; }
public MuxOptions MuxOptions { get; set; } public MuxOptions? MuxOptions { get; set; }
//public bool LiveWriteHLS { get; set; } = true; // public bool LiveWriteHLS { get; set; } = true;
/// <summary> /// <summary>
/// See: <see cref="CommandInvoker.LivePipeMux"/>. /// See: <see cref="CommandInvoker.LivePipeMux"/>.
/// </summary> /// </summary>
@ -254,5 +271,4 @@ namespace N_m3u8DL_RE.CommandLine
/// See: <see cref="CommandInvoker.LiveFixVttByAudio"/>. /// See: <see cref="CommandInvoker.LiveFixVttByAudio"/>.
/// </summary> /// </summary>
public bool LiveFixVttByAudio { get; set; } public bool LiveFixVttByAudio { get; set; }
}
} }

View File

@ -1,16 +1,9 @@
using N_m3u8DL_RE.CommandLine; using N_m3u8DL_RE.CommandLine;
using N_m3u8DL_RE.Enum;
using N_m3u8DL_RE.Parser.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Config namespace N_m3u8DL_RE.Config;
internal class DownloaderConfig
{ {
internal class DownloaderConfig
{
public required MyOption MyOptions { get; set; } public required MyOption MyOptions { get; set; }
/// <summary> /// <summary>
@ -29,5 +22,4 @@ namespace N_m3u8DL_RE.Config
/// 请求头 /// 请求头
/// </summary> /// </summary>
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(); public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
}
} }

View File

@ -1,14 +1,9 @@
using System; using System.Security.Cryptography;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Crypto namespace N_m3u8DL_RE.Crypto;
internal static class AESUtil
{ {
internal class AESUtil
{
/// <summary> /// <summary>
/// AES-128解密解密后原地替换文件 /// AES-128解密解密后原地替换文件
/// </summary> /// </summary>
@ -40,5 +35,4 @@ namespace N_m3u8DL_RE.Crypto
byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length);
return resultArray; return resultArray;
} }
}
} }

View File

@ -1,14 +1,9 @@
using CSChaCha20; using CSChaCha20;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Crypto namespace N_m3u8DL_RE.Crypto;
internal static class ChaCha20Util
{ {
internal class ChaCha20Util
{
public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes) public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes)
{ {
if (keyBytes.Length != 32) if (keyBytes.Length != 32)
@ -39,5 +34,4 @@ namespace N_m3u8DL_RE.Crypto
return decStream.ToArray(); return decStream.ToArray();
} }
}
} }

View File

@ -11,12 +11,12 @@
<SatelliteResourceLanguages>zh-CN;zh-TW;en-US</SatelliteResourceLanguages> <SatelliteResourceLanguages>zh-CN;zh-TW;en-US</SatelliteResourceLanguages>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols> <StripSymbols>true</StripSymbols>
<ObjCopyName Condition="'$(RuntimeIdentifier)' == 'linux-arm64'">aarch64-linux-gnu-objcopy</ObjCopyName> <ObjCopyName Condition="'$(RuntimeIdentifier)' == 'linux-arm64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">aarch64-linux-gnu-objcopy</ObjCopyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(PublishAot)' == 'true' and '$(RuntimeIdentifier)' != 'win-arm64' and '$(RuntimeIdentifier)' != 'linux-arm64' and '$(RuntimeIdentifier)' != 'osx-arm64' and '$(RuntimeIdentifier)' != 'osx-x64'"> <!--<ItemGroup Condition="'$(PublishAot)' == 'true' and '$(RuntimeIdentifier)' != 'win-arm64' and '$(RuntimeIdentifier)' != 'linux-arm64' and '$(RuntimeIdentifier)' != 'osx-arm64' and '$(RuntimeIdentifier)' != 'osx-x64'">
<PackageReference Include="PublishAotCompressed" Version="1.0.0" /> <PackageReference Include="PublishAotCompressed" Version="1.0.3" />
</ItemGroup> </ItemGroup>-->
<ItemGroup> <ItemGroup>
<RdXmlFile Include="rd.xml" /> <RdXmlFile Include="rd.xml" />

View File

@ -1,7 +1,5 @@
using Mp4SubtitleParser; using N_m3u8DL_RE.Column;
using N_m3u8DL_RE.Column;
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
@ -11,35 +9,26 @@ using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Parser; using N_m3u8DL_RE.Parser;
using N_m3u8DL_RE.Util; using N_m3u8DL_RE.Util;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.IO;
using System.Net.Http.Headers;
using System.Reflection.PortableExecutable;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Xml.Linq;
namespace N_m3u8DL_RE.DownloadManager namespace N_m3u8DL_RE.DownloadManager;
internal class HTTPLiveRecordManager
{ {
internal class HTTPLiveRecordManager
{
IDownloader Downloader; IDownloader Downloader;
DownloaderConfig DownloaderConfig; DownloaderConfig DownloaderConfig;
StreamExtractor StreamExtractor; StreamExtractor StreamExtractor;
List<StreamSpec> SelectedSteams; List<StreamSpec> SelectedSteams;
List<OutputFile> OutputFiles = new(); List<OutputFile> OutputFiles = [];
DateTime NowDateTime; DateTime NowDateTime;
DateTime? PublishDateTime; DateTime? PublishDateTime;
bool STOP_FLAG = false; bool STOP_FLAG = false;
bool READ_IFO = false; bool READ_IFO = false;
ConcurrentDictionary<int, int> RecordingDurDic = new(); //已录制时长 ConcurrentDictionary<int, int> RecordingDurDic = new(); // 已录制时长
ConcurrentDictionary<int, double> RecordingSizeDic = new(); //已录制大小 ConcurrentDictionary<int, double> RecordingSizeDic = new(); // 已录制大小
CancellationTokenSource CancellationTokenSource = new(); //取消Wait CancellationTokenSource CancellationTokenSource = new(); // 取消Wait
List<byte> InfoBuffer = new List<byte>(188 * 5000); //5000个分包中解析信息没有就算了 List<byte> InfoBuffer = new List<byte>(188 * 5000); // 5000个分包中解析信息没有就算了
public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
{ {
@ -63,7 +52,7 @@ namespace N_m3u8DL_RE.DownloadManager
Logger.Debug($"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}"); Logger.Debug($"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}");
//创建文件夹 // 创建文件夹
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url)); using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url));
@ -83,10 +72,10 @@ namespace N_m3u8DL_RE.DownloadManager
var buffer = new byte[16 * 1024]; var buffer = new byte[16 * 1024];
var size = 0; var size = 0;
//计时器 // 计时器
TimeCounterAsync(); _ = TimeCounterAsync();
//读取INFO // 读取INFO
ReadInfoAsync(); _ = ReadInfoAsync();
try try
{ {
@ -118,7 +107,7 @@ namespace N_m3u8DL_RE.DownloadManager
await Task.Delay(200); await Task.Delay(200);
if (InfoBuffer.Count < 188 * 5000) continue; if (InfoBuffer.Count < 188 * 5000) continue;
UInt16 ConvertToUint16(IEnumerable<byte> bytes) ushort ConvertToUint16(IEnumerable<byte> bytes)
{ {
if (BitConverter.IsLittleEndian) if (BitConverter.IsLittleEndian)
bytes = bytes.Reverse(); bytes = bytes.Reverse();
@ -137,16 +126,16 @@ namespace N_m3u8DL_RE.DownloadManager
var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0); var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0);
var pid = (tsHeaderInt & 0x1fff00) >> 8; var pid = (tsHeaderInt & 0x1fff00) >> 8;
var tsPayload = tsData.Skip(4); var tsPayload = tsData.Skip(4);
//PAT // PAT
if (pid == 0x0000) if (pid == 0x0000)
{ {
programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString(); programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString();
} }
//SDT, BAT, ST // SDT, BAT, ST
else if (pid == 0x0011) else if (pid == 0x0011)
{ {
var tableId = (int)tsPayload.Skip(1).First(); var tableId = (int)tsPayload.Skip(1).First();
//Current TS Info // Current TS Info
if (tableId == 0x42) if (tableId == 0x42)
{ {
var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff; var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff;
@ -182,7 +171,7 @@ namespace N_m3u8DL_RE.DownloadManager
await Task.Delay(1000); await Task.Delay(1000);
RecordingDurDic[0]++; RecordingDurDic[0]++;
//检测时长限制 // 检测时长限制
if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds)) if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds))
{ {
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
@ -194,20 +183,20 @@ namespace N_m3u8DL_RE.DownloadManager
public async Task<bool> StartRecordAsync() public async Task<bool> StartRecordAsync()
{ {
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); //速度计算 ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算
ConcurrentDictionary<StreamSpec, bool?> Results = new(); ConcurrentDictionary<StreamSpec, bool?> Results = new();
var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;
//进度条的列定义 // 进度条的列定义
var progressColumns = new ProgressColumn[] var progressColumns = new ProgressColumn[]
{ {
new TaskDescriptionColumn() { Alignment = Justify.Left }, new TaskDescriptionColumn() { Alignment = Justify.Left },
new RecordingDurationColumn(RecordingDurDic), //时长显示 new RecordingDurationColumn(RecordingDurDic), // 时长显示
new RecordingSizeColumn(RecordingSizeDic), //大小显示 new RecordingSizeColumn(RecordingSizeDic), // 大小显示
new RecordingStatusColumn(), new RecordingStatusColumn(),
new DownloadSpeedColumn(SpeedContainerDic), //速度计算 new DownloadSpeedColumn(SpeedContainerDic), // 速度计算
new SpinnerColumn(), new SpinnerColumn(),
}; };
if (DownloaderConfig.MyOptions.NoAnsiColor) if (DownloaderConfig.MyOptions.NoAnsiColor)
@ -218,26 +207,26 @@ namespace N_m3u8DL_RE.DownloadManager
await progress.StartAsync(async ctx => await progress.StartAsync(async ctx =>
{ {
//创建任务 // 创建任务
var dic = SelectedSteams.Select(item => var dic = SelectedSteams.Select(item =>
{ {
var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0); var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0);
SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算
RecordingDurDic[task.Id] = 0; RecordingDurDic[task.Id] = 0;
RecordingSizeDic[task.Id] = 0; RecordingSizeDic[task.Id] = 0;
return (item, task); return (item, task);
}).ToDictionary(item => item.item, item => item.task); }).ToDictionary(item => item.item, item => item.task);
DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;
var limit = DownloaderConfig.MyOptions.LiveRecordLimit; var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
if (limit != TimeSpan.MaxValue) if (limit != TimeSpan.MaxValue)
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");
//录制直播时,用户选了几个流就并发录几个 // 录制直播时,用户选了几个流就并发录几个
var options = new ParallelOptions() var options = new ParallelOptions()
{ {
MaxDegreeOfParallelism = SelectedSteams.Count MaxDegreeOfParallelism = SelectedSteams.Count
}; };
//并发下载 // 并发下载
await Parallel.ForEachAsync(dic, options, async (kp, _) => await Parallel.ForEachAsync(dic, options, async (kp, _) =>
{ {
var task = kp.Value; var task = kp.Value;
@ -250,5 +239,4 @@ namespace N_m3u8DL_RE.DownloadManager
return success; return success;
} }
}
} }

View File

@ -13,16 +13,17 @@ using N_m3u8DL_RE.Util;
using Spectre.Console; using Spectre.Console;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text; using System.Text;
using N_m3u8DL_RE.Enum;
namespace N_m3u8DL_RE.DownloadManager namespace N_m3u8DL_RE.DownloadManager;
internal class SimpleDownloadManager
{ {
internal class SimpleDownloadManager
{
IDownloader Downloader; IDownloader Downloader;
DownloaderConfig DownloaderConfig; DownloaderConfig DownloaderConfig;
StreamExtractor StreamExtractor; StreamExtractor StreamExtractor;
List<StreamSpec> SelectedSteams; List<StreamSpec> SelectedSteams;
List<OutputFile> OutputFiles = new(); List<OutputFile> OutputFiles = [];
public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) public SimpleDownloadManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
{ {
@ -32,28 +33,28 @@ namespace N_m3u8DL_RE.DownloadManager
Downloader = new SimpleDownloader(DownloaderConfig); Downloader = new SimpleDownloader(DownloaderConfig);
} }
//从文件读取KEY // 从文件读取KEY
private async Task SearchKeyAsync(string? currentKID) private async Task SearchKeyAsync(string? currentKID)
{ {
var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID); var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID);
if (_key != null) if (_key != null)
{ {
if (DownloaderConfig.MyOptions.Keys == null) if (DownloaderConfig.MyOptions.Keys == null)
DownloaderConfig.MyOptions.Keys = new string[] { _key }; DownloaderConfig.MyOptions.Keys = [_key];
else else
DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray(); DownloaderConfig.MyOptions.Keys = [..DownloaderConfig.MyOptions.Keys, _key];
} }
} }
private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter) private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)
{ {
if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))
{ {
DownloaderConfig.MyOptions.BinaryMerge = true; DownloaderConfig.MyOptions.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]");
} }
if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true)) if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))
{ {
DownloaderConfig.MyOptions.MuxAfterDone = false; DownloaderConfig.MyOptions.MuxAfterDone = false;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
@ -71,7 +72,7 @@ namespace N_m3u8DL_RE.DownloadManager
else if (mediainfos.All(m => m.Type == "Subtitle")) else if (mediainfos.All(m => m.Type == "Subtitle"))
{ {
streamSpec.MediaType = MediaType.SUBTITLES; streamSpec.MediaType = MediaType.SUBTITLES;
if (streamSpec.Extension == null || streamSpec.Extension == "ts") if (streamSpec.Extension is null or "ts")
streamSpec.Extension = "vtt"; streamSpec.Extension = "vtt";
} }
} }
@ -79,13 +80,13 @@ namespace N_m3u8DL_RE.DownloadManager
private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer) private async Task<bool> DownloadStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)
{ {
speedContainer.ResetVars(); speedContainer.ResetVars();
bool useAACFilter = false; //ffmpeg合并flag bool useAACFilter = false; // ffmpeg合并flag
List<Mediainfo> mediaInfos = new(); List<Mediainfo> mediaInfos = [];
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new(); ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments); var segments = streamSpec.Playlist?.MediaParts.SelectMany(m => m.MediaSegments);
if (segments == null || !segments.Any()) return false; if (segments == null || !segments.Any()) return false;
//单分段尝试切片并行下载 // 单分段尝试切片并行下载
if (segments.Count() == 1) if (segments.Count() == 1)
{ {
var splitSegments = await LargeSingleFileSplitUtil.SplitUrlAsync(segments.First(), DownloaderConfig.Headers); var splitSegments = await LargeSingleFileSplitUtil.SplitUrlAsync(segments.First(), DownloaderConfig.Headers);
@ -109,19 +110,19 @@ namespace N_m3u8DL_RE.DownloadManager
var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName; var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName;
var headers = DownloaderConfig.Headers; var headers = DownloaderConfig.Headers;
//mp4decrypt var decryptionBinaryPath = DownloaderConfig.MyOptions.DecryptionBinaryPath!;
var mp4decrypt = DownloaderConfig.MyOptions.DecryptionBinaryPath!; var decryptEngine = DownloaderConfig.MyOptions.DecryptionEngine;
var mp4InitFile = ""; var mp4InitFile = "";
var currentKID = ""; var currentKID = "";
var readInfo = false; //是否读取过 var readInfo = false; // 是否读取过
var mp4Info = new ParsedMP4Info(); var mp4Info = new ParsedMP4Info();
//用户自定义范围导致被跳过的时长 计算字幕偏移使用 // 用户自定义范围导致被跳过的时长 计算字幕偏移使用
var skippedDur = streamSpec.SkippedDuration ?? 0d; var skippedDur = streamSpec.SkippedDuration ?? 0d;
Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}");
//创建文件夹 // 创建文件夹
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
@ -134,20 +135,20 @@ namespace N_m3u8DL_RE.DownloadManager
task.MaxValue = totalCount; task.MaxValue = totalCount;
task.StartTask(); task.StartTask();
//开始下载 // 开始下载
Logger.InfoMarkUp(ResString.startDownloading + streamSpec.ToShortString()); Logger.InfoMarkUp(ResString.startDownloading + streamSpec.ToShortString());
//对于CENC全部自动开启二进制合并 // 对于CENC全部自动开启二进制合并
if (!DownloaderConfig.MyOptions.BinaryMerge && totalCount >= 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method == Common.Enum.EncryptMethod.CENC) if (!DownloaderConfig.MyOptions.BinaryMerge && totalCount >= 1 && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method == Common.Enum.EncryptMethod.CENC)
{ {
DownloaderConfig.MyOptions.BinaryMerge = true; DownloaderConfig.MyOptions.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge4}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge4}[/]");
} }
//下载init // 下载init
if (streamSpec.Playlist?.MediaInit != null) if (streamSpec.Playlist?.MediaInit != null)
{ {
//对于fMP4自动开启二进制合并 // 对于fMP4自动开启二进制合并
if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES)
{ {
DownloaderConfig.MyOptions.BinaryMerge = true; DownloaderConfig.MyOptions.BinaryMerge = true;
@ -157,36 +158,36 @@ namespace N_m3u8DL_RE.DownloadManager
var path = Path.Combine(tmpDir, "_init.mp4.tmp"); var path = Path.Combine(tmpDir, "_init.mp4.tmp");
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
FileDic[streamSpec.Playlist.MediaInit] = result; FileDic[streamSpec.Playlist.MediaInit] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download init file failed!"); throw new Exception("Download init file failed!");
} }
mp4InitFile = result.ActualFilePath; mp4InitFile = result.ActualFilePath;
task.Increment(1); task.Increment(1);
//读取mp4信息 // 读取mp4信息
if (result != null && result.Success) if (result is { Success: true })
{ {
mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath); mp4Info = MP4DecryptUtil.GetMP4Info(result.ActualFilePath);
currentKID = mp4Info.KID; currentKID = mp4Info.KID;
// try shaka packager, which can handle WebM // try shaka packager, which can handle WebM
if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {
currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, decryptionBinaryPath);
} }
//从文件读取KEY // 从文件读取KEY
await SearchKeyAsync(currentKID); await SearchKeyAsync(currentKID);
//实时解密 // 实时解密
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS) if ((streamSpec.Playlist.MediaInit.IsEncrypted || !string.IsNullOrEmpty(currentKID)) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS)
{ {
var enc = result.ActualFilePath; var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM);
if (dResult) if (dResult)
{ {
FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec;
} }
} }
//ffmpeg读取信息 // ffmpeg读取信息
if (!readInfo) if (!readInfo)
{ {
Logger.WarnMarkUp(ResString.readingInfo); Logger.WarnMarkUp(ResString.readingInfo);
@ -198,10 +199,10 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//计算填零个数 // 计算填零个数
var pad = "0".PadLeft(segments.Count().ToString().Length, '0'); var pad = "0".PadLeft(segments.Count().ToString().Length, '0');
//下载第一个分片 // 下载第一个分片
if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS) if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS)
{ {
var seg = segments.First(); var seg = segments.First();
@ -211,49 +212,49 @@ namespace N_m3u8DL_RE.DownloadManager
var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download first segment failed!"); throw new Exception("Download first segment failed!");
} }
task.Increment(1); task.Increment(1);
if (result != null && result.Success) if (result is { Success: true })
{ {
//修复MSS init // 修复MSS init
if (StreamExtractor.ExtractorType == ExtractorType.MSS) if (StreamExtractor.ExtractorType == ExtractorType.MSS)
{ {
var processor = new MSSMoovProcessor(streamSpec); var processor = new MSSMoovProcessor(streamSpec);
var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath)); var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath));
await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header); await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header);
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))
{ {
//需要重新解密init // 需要重新解密init
var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath; var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);
if (dResult) if (dResult)
{ {
FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec; FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec;
} }
} }
} }
//读取init信息 // 读取init信息
if (string.IsNullOrEmpty(currentKID)) if (string.IsNullOrEmpty(currentKID))
{ {
currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;
} }
// try shaka packager, which can handle WebM // try shaka packager, which can handle WebM
if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {
currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, mp4decrypt); currentKID = MP4DecryptUtil.ReadInitShaka(result.ActualFilePath, decryptionBinaryPath);
} }
//从文件读取KEY // 从文件读取KEY
await SearchKeyAsync(currentKID); await SearchKeyAsync(currentKID);
//实时解密 // 实时解密
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))
{ {
var enc = result.ActualFilePath; var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
mp4Info = MP4DecryptUtil.GetMP4Info(enc); mp4Info = MP4DecryptUtil.GetMP4Info(enc);
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM);
if (dResult) if (dResult)
{ {
File.Delete(enc); File.Delete(enc);
@ -262,7 +263,7 @@ namespace N_m3u8DL_RE.DownloadManager
} }
if (!readInfo) if (!readInfo)
{ {
//ffmpeg读取信息 // ffmpeg读取信息
Logger.WarnMarkUp(ResString.readingInfo); Logger.WarnMarkUp(ResString.readingInfo);
mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath);
mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));
@ -272,7 +273,7 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//开始下载 // 开始下载
var options = new ParallelOptions() var options = new ParallelOptions()
{ {
MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount
@ -283,15 +284,15 @@ namespace N_m3u8DL_RE.DownloadManager
var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp"); var path = Path.Combine(tmpDir, index.ToString(pad) + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result != null && result.Success) if (result is { Success: true })
task.Increment(1); task.Increment(1);
//实时解密 // 实时解密
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID)) if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result is { Success: true } && !string.IsNullOrEmpty(currentKID))
{ {
var enc = result.ActualFilePath; var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
mp4Info = MP4DecryptUtil.GetMP4Info(enc); mp4Info = MP4DecryptUtil.GetMP4Info(enc);
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile, isMultiDRM: mp4Info.isMultiDRM);
if (dResult) if (dResult)
{ {
File.Delete(enc); File.Delete(enc);
@ -300,61 +301,59 @@ namespace N_m3u8DL_RE.DownloadManager
} }
}); });
//修改输出后缀 // 修改输出后缀
var outputExt = "." + streamSpec.Extension; var outputExt = "." + streamSpec.Extension;
if (streamSpec.Extension == null) outputExt = ".ts"; if (streamSpec.Extension == null) outputExt = ".ts";
else if (streamSpec.MediaType == MediaType.AUDIO && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".m4a"; else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: "m4s" or "mp4" }) outputExt = ".m4a";
else if (streamSpec.MediaType != MediaType.SUBTITLES && (streamSpec.Extension == "m4s" || streamSpec.Extension == "mp4")) outputExt = ".mp4"; else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension is "m4s" or "mp4") outputExt = ".mp4";
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES) if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == MediaType.SUBTITLES)
{ {
if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? ".srt" : ".vtt";
else outputExt = ".vtt";
} }
var output = Path.Combine(saveDir, saveName + outputExt); var output = Path.Combine(saveDir, saveName + outputExt);
//检测目标文件是否存在 // 检测目标文件是否存在
while (File.Exists(output)) while (File.Exists(output))
{ {
Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}");
} }
if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0 && mp4InitFile != "") if (!string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, Keys.Length: > 0 } && mp4InitFile != "")
{ {
File.Delete(mp4InitFile); File.Delete(mp4InitFile);
//shaka实时解密不需要init文件用于合并 // shaka/ffmpeg实时解密不需要init文件用于合并
if (DownloaderConfig.MyOptions.UseShakaPackager) if (decryptEngine != DecryptEngine.MP4DECRYPT)
{ {
FileDic!.Remove(streamSpec.Playlist!.MediaInit, out _); FileDic!.Remove(streamSpec.Playlist!.MediaInit, out _);
} }
} }
//校验分片数量 // 校验分片数量
if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null)) if (DownloaderConfig.MyOptions.CheckSegmentsCount && FileDic.Values.Any(s => s == null))
{ {
Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Where(s => s != null).Count()); Logger.ErrorMarkUp(ResString.segmentCountCheckNotPass, totalCount, FileDic.Values.Count(s => s != null));
return false; return false;
} }
//移除无效片段 // 移除无效片段
var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key); var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key);
foreach (var badKey in badKeys) foreach (var badKey in badKeys)
{ {
FileDic!.Remove(badKey, out _); FileDic!.Remove(badKey, out _);
} }
//校验完整性 // 校验完整性
if (DownloaderConfig.CheckContentLength && FileDic.Values.Any(a => a!.Success == false)) if (DownloaderConfig.CheckContentLength && FileDic.Values.Any(a => a!.Success == false))
{ {
return false; return false;
} }
//自动修复VTT raw字幕 // 自动修复VTT raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("vtt"))
&& streamSpec.Extension != null && streamSpec.Extension.Contains("vtt"))
{ {
Logger.WarnMarkUp(ResString.fixingVTT); Logger.WarnMarkUp(ResString.fixingVTT);
//排序字幕并修正时间戳 // 排序字幕并修正时间戳
bool first = true; bool first = true;
var finalVtt = new WebVttSub(); var finalVtt = new WebVttSub();
var keys = FileDic.Keys.OrderBy(k => k.Index); var keys = FileDic.Keys.OrderBy(k => k.Index);
@ -362,7 +361,7 @@ namespace N_m3u8DL_RE.DownloadManager
{ {
var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath);
var vtt = WebVttSub.Parse(vttContent); var vtt = WebVttSub.Parse(vttContent);
//手动计算MPEGTS // 手动计算MPEGTS
if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
@ -370,16 +369,16 @@ namespace N_m3u8DL_RE.DownloadManager
if (first) { finalVtt = vtt; first = false; } if (first) { finalVtt = vtt; first = false; }
else finalVtt.AddCuesFromOne(vtt); else finalVtt.AddCuesFromOne(vtt);
} }
//写出字幕 // 写出字幕
var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
foreach (var item in files) File.Delete(item); foreach (var item in files) File.Delete(item);
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
//设置字幕偏移 // 设置字幕偏移
finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
var subContentFixed = finalVtt.ToVtt(); var subContentFixed = finalVtt.ToVtt();
//转换字幕格式 // 转换字幕格式
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
@ -393,11 +392,11 @@ namespace N_m3u8DL_RE.DownloadManager
}; };
} }
//自动修复VTT mp4字幕 // 自动修复VTT mp4字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s"))
{ {
var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); var initFile = FileDic.Values.FirstOrDefault(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init"));
var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
if (sawVtt) if (sawVtt)
@ -405,17 +404,17 @@ namespace N_m3u8DL_RE.DownloadManager
Logger.WarnMarkUp(ResString.fixingVTTmp4); Logger.WarnMarkUp(ResString.fixingVTTmp4);
var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray(); var mp4s = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).Where(p => p.EndsWith(".m4s")).ToArray();
var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale); var finalVtt = MP4VttUtil.ExtractSub(mp4s, timescale);
//写出字幕 // 写出字幕
var firstKey = FileDic.Keys.First(); var firstKey = FileDic.Keys.First();
var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
foreach (var item in files) File.Delete(item); foreach (var item in files) File.Delete(item);
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
//设置字幕偏移 // 设置字幕偏移
finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
var subContentFixed = finalVtt.ToVtt(); var subContentFixed = finalVtt.ToVtt();
//转换字幕格式 // 转换字幕格式
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
@ -430,9 +429,8 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//自动修复TTML raw字幕 // 自动修复TTML raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("ttml"))
&& streamSpec.Extension != null && streamSpec.Extension.Contains("ttml"))
{ {
Logger.WarnMarkUp(ResString.fixingTTML); Logger.WarnMarkUp(ResString.fixingTTML);
var first = true; var first = true;
@ -441,7 +439,7 @@ namespace N_m3u8DL_RE.DownloadManager
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0); var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0);
//手动计算MPEGTS // 手动计算MPEGTS
if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
@ -449,21 +447,21 @@ namespace N_m3u8DL_RE.DownloadManager
if (first) { finalVtt = vtt; first = false; } if (first) { finalVtt = vtt; first = false; }
else finalVtt.AddCuesFromOne(vtt); else finalVtt.AddCuesFromOne(vtt);
} }
//写出字幕 // 写出字幕
var firstKey = FileDic.Keys.First(); var firstKey = FileDic.Keys.First();
var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
//处理图形字幕 // 处理图形字幕
await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir); await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir);
foreach (var item in files) File.Delete(item); foreach (var item in files) File.Delete(item);
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
//设置字幕偏移 // 设置字幕偏移
finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
var subContentFixed = finalVtt.ToVtt(); var subContentFixed = finalVtt.ToVtt();
//转换字幕格式 // 转换字幕格式
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
@ -477,23 +475,22 @@ namespace N_m3u8DL_RE.DownloadManager
}; };
} }
//自动修复TTML mp4字幕 // 自动修复TTML mp4字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("m4s")
&& streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
{ {
Logger.WarnMarkUp(ResString.fixingTTMLmp4); Logger.WarnMarkUp(ResString.fixingTTMLmp4);
//sawTtml暂时不判断 // sawTtml暂时不判断
//var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
//var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); // var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
//var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); // var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);
var first = true; var first = true;
var finalVtt = new WebVttSub(); var finalVtt = new WebVttSub();
var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key);
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0); var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0);
//手动计算MPEGTS // 手动计算MPEGTS
if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (finalVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); vtt.MpegtsTimestamp = 90000 * (long)(skippedDur + keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
@ -502,21 +499,21 @@ namespace N_m3u8DL_RE.DownloadManager
else finalVtt.AddCuesFromOne(vtt); else finalVtt.AddCuesFromOne(vtt);
} }
//写出字幕 // 写出字幕
var firstKey = FileDic.Keys.First(); var firstKey = FileDic.Keys.First();
var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
//处理图形字幕 // 处理图形字幕
await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir); await SubtitleUtil.TryWriteImagePngsAsync(finalVtt, tmpDir);
foreach (var item in files) File.Delete(item); foreach (var item in files) File.Delete(item);
FileDic.Clear(); FileDic.Clear();
var index = 0; var index = 0;
var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt"); var path = Path.Combine(tmpDir, index.ToString(pad) + ".fix.vtt");
//设置字幕偏移 // 设置字幕偏移
finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur)); finalVtt.LeftShiftTime(TimeSpan.FromSeconds(skippedDur));
var subContentFixed = finalVtt.ToVtt(); var subContentFixed = finalVtt.ToVtt();
//转换字幕格式 // 转换字幕格式
if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT) if (DownloaderConfig.MyOptions.SubtitleFormat != Enum.SubtitleFormat.VTT)
{ {
path = Path.ChangeExtension(path, ".srt"); path = Path.ChangeExtension(path, ".srt");
@ -531,10 +528,10 @@ namespace N_m3u8DL_RE.DownloadManager
} }
bool mergeSuccess = false; bool mergeSuccess = false;
//合并 // 合并
if (!DownloaderConfig.MyOptions.SkipMerge) if (!DownloaderConfig.MyOptions.SkipMerge)
{ {
//字幕也使用二进制合并 // 字幕也使用二进制合并
if (DownloaderConfig.MyOptions.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES) if (DownloaderConfig.MyOptions.BinaryMerge || streamSpec.MediaType == MediaType.SUBTITLES)
{ {
Logger.InfoMarkUp(ResString.binaryMerge); Logger.InfoMarkUp(ResString.binaryMerge);
@ -544,17 +541,17 @@ namespace N_m3u8DL_RE.DownloadManager
} }
else else
{ {
//ffmpeg合并 // ffmpeg合并
var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray(); var files = FileDic.OrderBy(s => s.Key.Index).Select(s => s.Value).Select(v => v!.ActualFilePath).ToArray();
Logger.InfoMarkUp(ResString.ffmpegMerge); Logger.InfoMarkUp(ResString.ffmpegMerge);
var ext = streamSpec.MediaType == MediaType.AUDIO ? "m4a" : "mp4"; var ext = streamSpec.MediaType == MediaType.AUDIO ? "m4a" : "mp4";
var ffOut = Path.Combine(Path.GetDirectoryName(output)!, Path.GetFileNameWithoutExtension(output) + $".{ext}"); var ffOut = Path.Combine(Path.GetDirectoryName(output)!, Path.GetFileNameWithoutExtension(output) + $".{ext}");
//检测目标文件是否存在 // 检测目标文件是否存在
while (File.Exists(ffOut)) while (File.Exists(ffOut))
{ {
Logger.WarnMarkUp($"{Path.GetFileName(ffOut)} => {Path.GetFileName(ffOut = Path.ChangeExtension(ffOut, $"copy" + Path.GetExtension(ffOut)))}"); Logger.WarnMarkUp($"{Path.GetFileName(ffOut)} => {Path.GetFileName(ffOut = Path.ChangeExtension(ffOut, $"copy" + Path.GetExtension(ffOut)))}");
} }
//大于1800分片需要分步骤合并 // 大于1800分片需要分步骤合并
if (files.Length >= 1800) if (files.Length >= 1800)
{ {
Logger.WarnMarkUp(ResString.partMerge); Logger.WarnMarkUp(ResString.partMerge);
@ -573,8 +570,8 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//删除临时文件夹 // 删除临时文件夹
if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && mergeSuccess) if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && mergeSuccess)
{ {
var files = FileDic.Values.Select(v => v!.ActualFilePath); var files = FileDic.Values.Select(v => v!.ActualFilePath);
foreach (var file in files) foreach (var file in files)
@ -584,26 +581,26 @@ namespace N_m3u8DL_RE.DownloadManager
OtherUtil.SafeDeleteDir(tmpDir); OtherUtil.SafeDeleteDir(tmpDir);
} }
//重新读取init信息 // 重新读取init信息
if (mergeSuccess && totalCount >= 1 && string.IsNullOrEmpty(currentKID) && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE) if (mergeSuccess && totalCount >= 1 && string.IsNullOrEmpty(currentKID) && streamSpec.Playlist!.MediaParts.First().MediaSegments.First().EncryptInfo.Method != Common.Enum.EncryptMethod.NONE)
{ {
currentKID = MP4DecryptUtil.GetMP4Info(output).KID; currentKID = MP4DecryptUtil.GetMP4Info(output).KID;
// try shaka packager, which can handle WebM // try shaka packager, which can handle WebM
if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.UseShakaPackager) { if (string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions.DecryptionEngine == DecryptEngine.SHAKA_PACKAGER) {
currentKID = MP4DecryptUtil.ReadInitShaka(output, mp4decrypt); currentKID = MP4DecryptUtil.ReadInitShaka(output, decryptionBinaryPath);
} }
//从文件读取KEY // 从文件读取KEY
await SearchKeyAsync(currentKID); await SearchKeyAsync(currentKID);
} }
//调用mp4decrypt解密 // 调用mp4decrypt解密
if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && !DownloaderConfig.MyOptions.MP4RealTimeDecryption && DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0) if (mergeSuccess && File.Exists(output) && !string.IsNullOrEmpty(currentKID) && DownloaderConfig.MyOptions is { MP4RealTimeDecryption: false, Keys.Length: > 0 })
{ {
var enc = output; var enc = output;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
mp4Info = MP4DecryptUtil.GetMP4Info(enc); mp4Info = MP4DecryptUtil.GetMP4Info(enc);
Logger.InfoMarkUp($"[grey]Decrypting...[/]"); Logger.InfoMarkUp($"[grey]Decrypting using {decryptEngine}...[/]");
var result = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM); var result = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, isMultiDRM: mp4Info.isMultiDRM);
if (result) if (result)
{ {
File.Delete(enc); File.Delete(enc);
@ -611,7 +608,7 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//记录所有文件信息 // 记录所有文件信息
if (File.Exists(output)) if (File.Exists(output))
{ {
OutputFiles.Add(new OutputFile() OutputFiles.Add(new OutputFile()
@ -630,20 +627,20 @@ namespace N_m3u8DL_RE.DownloadManager
public async Task<bool> StartDownloadAsync() public async Task<bool> StartDownloadAsync()
{ {
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); //速度计算 ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算
ConcurrentDictionary<StreamSpec, bool?> Results = new(); ConcurrentDictionary<StreamSpec, bool?> Results = new();
var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;
//进度条的列定义 // 进度条的列定义
var progressColumns = new ProgressColumn[] var progressColumns = new ProgressColumn[]
{ {
new TaskDescriptionColumn() { Alignment = Justify.Left }, new TaskDescriptionColumn() { Alignment = Justify.Left },
new ProgressBarColumn(){ Width = 30 }, new ProgressBarColumn(){ Width = 30 },
new MyPercentageColumn(), new MyPercentageColumn(),
new DownloadStatusColumn(SpeedContainerDic), new DownloadStatusColumn(SpeedContainerDic),
new DownloadSpeedColumn(SpeedContainerDic), //速度计算 new DownloadSpeedColumn(SpeedContainerDic), // 速度计算
new RemainingTimeColumn(), new RemainingTimeColumn(),
new SpinnerColumn(), new SpinnerColumn(),
}; };
@ -653,19 +650,18 @@ namespace N_m3u8DL_RE.DownloadManager
} }
progress.Columns(progressColumns); progress.Columns(progressColumns);
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !DownloaderConfig.MyOptions.UseShakaPackager if (DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, DecryptionEngine: not DecryptEngine.SHAKA_PACKAGER, Keys.Length: > 0 })
&& DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0)
Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]");
await progress.StartAsync(async ctx => await progress.StartAsync(async ctx =>
{ {
//创建任务 // 创建任务
var dic = SelectedSteams.Select(item => var dic = SelectedSteams.Select(item =>
{ {
var description = item.ToShortShortString(); var description = item.ToShortShortString();
var task = ctx.AddTask(description, autoStart: false); var task = ctx.AddTask(description, autoStart: false);
SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算
//限速设置 // 限速设置
if (DownloaderConfig.MyOptions.MaxSpeed != null) if (DownloaderConfig.MyOptions.MaxSpeed != null)
{ {
SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value; SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value;
@ -675,19 +671,19 @@ namespace N_m3u8DL_RE.DownloadManager
if (!DownloaderConfig.MyOptions.ConcurrentDownload) if (!DownloaderConfig.MyOptions.ConcurrentDownload)
{ {
//遍历,顺序下载 // 遍历,顺序下载
foreach (var kp in dic) foreach (var kp in dic)
{ {
var task = kp.Value; var task = kp.Value;
var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]); var result = await DownloadStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);
Results[kp.Key] = result; Results[kp.Key] = result;
//失败不再下载后续 // 失败不再下载后续
if (!result) break; if (!result) break;
} }
} }
else else
{ {
//并发下载 // 并发下载
await Parallel.ForEachAsync(dic, async (kp, _) => await Parallel.ForEachAsync(dic, async (kp, _) =>
{ {
var task = kp.Value; var task = kp.Value;
@ -699,8 +695,8 @@ namespace N_m3u8DL_RE.DownloadManager
var success = Results.Values.All(v => v == true); var success = Results.Values.All(v => v == true);
//删除临时文件夹 // 删除临时文件夹
if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success) if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && success)
{ {
foreach (var item in StreamExtractor.RawFiles) foreach (var item in StreamExtractor.RawFiles)
{ {
@ -710,12 +706,12 @@ namespace N_m3u8DL_RE.DownloadManager
OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix); OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix);
} }
//混流 // 混流
if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0)
{ {
OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList();
//是否跳过字幕 // 是否跳过字幕
if (DownloaderConfig.MyOptions.MuxOptions.SkipSubtitle) if (DownloaderConfig.MyOptions.MuxOptions!.SkipSubtitle)
{ {
OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList();
} }
@ -725,15 +721,15 @@ namespace N_m3u8DL_RE.DownloadManager
} }
OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]"));
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
var ext = DownloaderConfig.MyOptions.MuxOptions.MuxToMp4 ? ".mp4" : ".mkv"; var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat);
var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); var dirName = Path.GetFileName(DownloaderConfig.DirPrefix);
var outName = $"{dirName}.MUX"; var outName = $"{dirName}.MUX";
var outPath = Path.Combine(saveDir, outName); var outPath = Path.Combine(saveDir, outName);
Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]");
var result = false; var result = false;
if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath);
else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxToMp4, !DownloaderConfig.MyOptions.NoDateInfo); else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo);
//完成后删除各轨道文件 // 完成后删除各轨道文件
if (result) if (result)
{ {
if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles)
@ -749,7 +745,7 @@ namespace N_m3u8DL_RE.DownloadManager
success = false; success = false;
Logger.ErrorMarkUp($"Mux failed"); Logger.ErrorMarkUp($"Mux failed");
} }
//判断是否要改名 // 判断是否要改名
var newPath = Path.ChangeExtension(outPath, ext); var newPath = Path.ChangeExtension(outPath, ext);
if (result && !File.Exists(newPath)) if (result && !File.Exists(newPath))
{ {
@ -760,5 +756,4 @@ namespace N_m3u8DL_RE.DownloadManager
return success; return success;
} }
}
} }

View File

@ -12,41 +12,36 @@ using N_m3u8DL_RE.Parser;
using N_m3u8DL_RE.Parser.Mp4; using N_m3u8DL_RE.Parser.Mp4;
using N_m3u8DL_RE.Util; using N_m3u8DL_RE.Util;
using Spectre.Console; using Spectre.Console;
using Spectre.Console.Rendering;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes; using System.IO.Pipes;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow; using System.Threading.Tasks.Dataflow;
using System.Xml.Linq; using N_m3u8DL_RE.Enum;
namespace N_m3u8DL_RE.DownloadManager namespace N_m3u8DL_RE.DownloadManager;
internal class SimpleLiveRecordManager2
{ {
internal class SimpleLiveRecordManager2
{
IDownloader Downloader; IDownloader Downloader;
DownloaderConfig DownloaderConfig; DownloaderConfig DownloaderConfig;
StreamExtractor StreamExtractor; StreamExtractor StreamExtractor;
List<StreamSpec> SelectedSteams; List<StreamSpec> SelectedSteams;
ConcurrentDictionary<int, string> PipeSteamNamesDic = new(); ConcurrentDictionary<int, string> PipeSteamNamesDic = new();
List<OutputFile> OutputFiles = new(); List<OutputFile> OutputFiles = [];
DateTime? PublishDateTime; DateTime? PublishDateTime;
bool STOP_FLAG = false; bool STOP_FLAG = false;
int WAIT_SEC = 0; //刷新间隔 int WAIT_SEC = 0; // 刷新间隔
ConcurrentDictionary<int, int> RecordedDurDic = new(); //已录制时长 ConcurrentDictionary<int, int> RecordedDurDic = new(); // 已录制时长
ConcurrentDictionary<int, int> RefreshedDurDic = new(); //已刷新出的时长 ConcurrentDictionary<int, int> RefreshedDurDic = new(); // 已刷新出的时长
ConcurrentDictionary<int, BufferBlock<List<MediaSegment>>> BlockDic = new(); //各流的Block ConcurrentDictionary<int, BufferBlock<List<MediaSegment>>> BlockDic = new(); // 各流的Block
ConcurrentDictionary<int, bool> SamePathDic = new(); //各流是否allSamePath ConcurrentDictionary<int, bool> SamePathDic = new(); // 各流是否allSamePath
ConcurrentDictionary<int, bool> RecordLimitReachedDic = new(); //各流是否达到上限 ConcurrentDictionary<int, bool> RecordLimitReachedDic = new(); // 各流是否达到上限
ConcurrentDictionary<int, string> LastFileNameDic = new(); //上次下载的文件名 ConcurrentDictionary<int, string> LastFileNameDic = new(); // 上次下载的文件名
ConcurrentDictionary<int, long> MaxIndexDic = new(); //最大Index ConcurrentDictionary<int, long> MaxIndexDic = new(); // 最大Index
ConcurrentDictionary<int, long> DateTimeDic = new(); //上次下载的dateTime ConcurrentDictionary<int, long> DateTimeDic = new(); // 上次下载的dateTime
CancellationTokenSource CancellationTokenSource = new(); //取消Wait CancellationTokenSource CancellationTokenSource = new(); // 取消Wait
private readonly object lockObj = new object(); private readonly Lock lockObj = new();
TimeSpan? audioStart = null; TimeSpan? audioStart = null;
public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor) public SimpleLiveRecordManager2(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
@ -58,16 +53,16 @@ namespace N_m3u8DL_RE.DownloadManager
SelectedSteams = selectedSteams; SelectedSteams = selectedSteams;
} }
//从文件读取KEY // 从文件读取KEY
private async Task SearchKeyAsync(string? currentKID) private async Task SearchKeyAsync(string? currentKID)
{ {
var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID); var _key = await MP4DecryptUtil.SearchKeyFromFileAsync(DownloaderConfig.MyOptions.KeyTextFile, currentKID);
if (_key != null) if (_key != null)
{ {
if (DownloaderConfig.MyOptions.Keys == null) if (DownloaderConfig.MyOptions.Keys == null)
DownloaderConfig.MyOptions.Keys = new string[] { _key }; DownloaderConfig.MyOptions.Keys = [_key];
else else
DownloaderConfig.MyOptions.Keys = DownloaderConfig.MyOptions.Keys.Concat(new string[] { _key }).ToArray(); DownloaderConfig.MyOptions.Keys = [..DownloaderConfig.MyOptions.Keys, _key];
} }
} }
@ -116,13 +111,13 @@ namespace N_m3u8DL_RE.DownloadManager
private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter) private void ChangeSpecInfo(StreamSpec streamSpec, List<Mediainfo> mediainfos, ref bool useAACFilter)
{ {
if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison == true)) if (!DownloaderConfig.MyOptions.BinaryMerge && mediainfos.Any(m => m.DolbyVison))
{ {
DownloaderConfig.MyOptions.BinaryMerge = true; DownloaderConfig.MyOptions.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge2}[/]");
} }
if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison == true)) if (DownloaderConfig.MyOptions.MuxAfterDone && mediainfos.Any(m => m.DolbyVison))
{ {
DownloaderConfig.MyOptions.MuxAfterDone = false; DownloaderConfig.MyOptions.MuxAfterDone = false;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge5}[/]");
@ -141,7 +136,7 @@ namespace N_m3u8DL_RE.DownloadManager
{ {
streamSpec.MediaType = MediaType.SUBTITLES; streamSpec.MediaType = MediaType.SUBTITLES;
if (streamSpec.Extension == null || streamSpec.Extension == "ts") if (streamSpec.Extension is null or "ts")
streamSpec.Extension = "vtt"; streamSpec.Extension = "vtt";
} }
} }
@ -149,17 +144,16 @@ namespace N_m3u8DL_RE.DownloadManager
private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer, BufferBlock<List<MediaSegment>> source) private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer, BufferBlock<List<MediaSegment>> source)
{ {
var baseTimestamp = PublishDateTime == null ? 0L : (long)(PublishDateTime.Value.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds; var baseTimestamp = PublishDateTime == null ? 0L : (long)(PublishDateTime.Value.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalMilliseconds;
//mp4decrypt var decryptionBinaryPath = DownloaderConfig.MyOptions.DecryptionBinaryPath!;
var mp4decrypt = DownloaderConfig.MyOptions.DecryptionBinaryPath!;
var mp4InitFile = ""; var mp4InitFile = "";
var currentKID = ""; var currentKID = "";
var readInfo = false; //是否读取过 var readInfo = false; // 是否读取过
bool useAACFilter = false; //ffmpeg合并flag bool useAACFilter = false; // ffmpeg合并flag
bool initDownloaded = false; //是否下载过init文件 bool initDownloaded = false; // 是否下载过init文件
ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new(); ConcurrentDictionary<MediaSegment, DownloadResult?> FileDic = new();
List<Mediainfo> mediaInfos = new(); List<Mediainfo> mediaInfos = [];
Stream? fileOutputStream = null; Stream? fileOutputStream = null;
WebVttSub currentVtt = new(); //字幕流始终维护一个实例 WebVttSub currentVtt = new(); // 字幕流始终维护一个实例
bool firstSub = true; bool firstSub = true;
task.StartTask(); task.StartTask();
@ -170,29 +164,30 @@ namespace N_m3u8DL_RE.DownloadManager
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName; var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName;
var headers = DownloaderConfig.Headers; var headers = DownloaderConfig.Headers;
var decryptEngine = DownloaderConfig.MyOptions.DecryptionEngine;
Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}"); Logger.Debug($"dirName: {dirName}; tmpDir: {tmpDir}; saveDir: {saveDir}; saveName: {saveName}");
//创建文件夹 // 创建文件夹
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
while (true && await source.OutputAvailableAsync()) while (true && await source.OutputAvailableAsync())
{ {
//接收新片段 且总是拿全部未处理的片段 // 接收新片段 且总是拿全部未处理的片段
//有时每次只有很少的片段,但是之前的片段下载慢,导致后面还没下载的片段都失效了 // 有时每次只有很少的片段,但是之前的片段下载慢,导致后面还没下载的片段都失效了
//TryReceiveAll可以稍微缓解一下 // TryReceiveAll可以稍微缓解一下
source.TryReceiveAll(out IList<List<MediaSegment>>? segmentsList); source.TryReceiveAll(out IList<List<MediaSegment>>? segmentsList);
var segments = segmentsList!.SelectMany(s => s); var segments = segmentsList!.SelectMany(s => s);
if (segments == null || !segments.Any()) continue; if (segments == null || !segments.Any()) continue;
var segmentsDuration = segments.Sum(s => s.Duration); var segmentsDuration = segments.Sum(s => s.Duration);
Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false)))); Logger.DebugMarkUp(string.Join(",", segments.Select(sss => GetSegmentName(sss, false, false))));
//下载init // 下载init
if (!initDownloaded && streamSpec.Playlist?.MediaInit != null) if (!initDownloaded && streamSpec.Playlist?.MediaInit != null)
{ {
task.MaxValue += 1; task.MaxValue += 1;
//对于fMP4自动开启二进制合并 // 对于fMP4自动开启二进制合并
if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES) if (!DownloaderConfig.MyOptions.BinaryMerge && streamSpec.MediaType != MediaType.SUBTITLES)
{ {
DownloaderConfig.MyOptions.BinaryMerge = true; DownloaderConfig.MyOptions.BinaryMerge = true;
@ -202,31 +197,31 @@ namespace N_m3u8DL_RE.DownloadManager
var path = Path.Combine(tmpDir, "_init.mp4.tmp"); var path = Path.Combine(tmpDir, "_init.mp4.tmp");
var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(streamSpec.Playlist.MediaInit, path, speedContainer, headers);
FileDic[streamSpec.Playlist.MediaInit] = result; FileDic[streamSpec.Playlist.MediaInit] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download init file failed!"); throw new Exception("Download init file failed!");
} }
mp4InitFile = result.ActualFilePath; mp4InitFile = result.ActualFilePath;
task.Increment(1); task.Increment(1);
//读取mp4信息 // 读取mp4信息
if (result != null && result.Success) if (result is { Success: true })
{ {
currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;
//从文件读取KEY // 从文件读取KEY
await SearchKeyAsync(currentKID); await SearchKeyAsync(currentKID);
//实时解密 // 实时解密
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS) if ((streamSpec.Playlist.MediaInit.IsEncrypted || !string.IsNullOrEmpty(currentKID)) && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID) && StreamExtractor.ExtractorType != ExtractorType.MSS)
{ {
var enc = result.ActualFilePath; var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);
if (dResult) if (dResult)
{ {
FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec; FileDic[streamSpec.Playlist.MediaInit]!.ActualFilePath = dec;
} }
} }
//ffmpeg读取信息 // ffmpeg读取信息
if (!readInfo) if (!readInfo)
{ {
Logger.WarnMarkUp(ResString.readingInfo); Logger.WarnMarkUp(ResString.readingInfo);
@ -251,55 +246,55 @@ namespace N_m3u8DL_RE.DownloadManager
SamePathDic[task.Id] = allSamePath; SamePathDic[task.Id] = allSamePath;
} }
//下载第一个分片 // 下载第一个分片
if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS) if (!readInfo || StreamExtractor.ExtractorType == ExtractorType.MSS)
{ {
var seg = segments.First(); var seg = segments.First();
segments = segments.Skip(1); segments = segments.Skip(1);
//获取文件名 // 获取文件名
var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]); var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]);
var index = seg.Index; var index = seg.Index;
var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result == null || !result.Success) if (result is not { Success: true })
{ {
throw new Exception("Download first segment failed!"); throw new Exception("Download first segment failed!");
} }
task.Increment(1); task.Increment(1);
if (result != null && result.Success) if (result is { Success: true })
{ {
//修复MSS init // 修复MSS init
if (StreamExtractor.ExtractorType == ExtractorType.MSS) if (StreamExtractor.ExtractorType == ExtractorType.MSS)
{ {
var processor = new MSSMoovProcessor(streamSpec); var processor = new MSSMoovProcessor(streamSpec);
var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath)); var header = processor.GenHeader(File.ReadAllBytes(result.ActualFilePath));
await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header); await File.WriteAllBytesAsync(FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath, header);
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))
{ {
//需要重新解密init // 需要重新解密init
var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath; var enc = FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID);
if (dResult) if (dResult)
{ {
FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec; FileDic[streamSpec.Playlist!.MediaInit!]!.ActualFilePath = dec;
} }
} }
} }
//读取init信息 // 读取init信息
if (string.IsNullOrEmpty(currentKID)) if (string.IsNullOrEmpty(currentKID))
{ {
currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID; currentKID = MP4DecryptUtil.GetMP4Info(result.ActualFilePath).KID;
} }
//从文件读取KEY // 从文件读取KEY
await SearchKeyAsync(currentKID); await SearchKeyAsync(currentKID);
//实时解密 // 实时解密
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID)) if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && !string.IsNullOrEmpty(currentKID))
{ {
var enc = result.ActualFilePath; var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile);
if (dResult) if (dResult)
{ {
File.Delete(enc); File.Delete(enc);
@ -308,7 +303,7 @@ namespace N_m3u8DL_RE.DownloadManager
} }
if (!readInfo) if (!readInfo)
{ {
//ffmpeg读取信息 // ffmpeg读取信息
Logger.WarnMarkUp(ResString.readingInfo); Logger.WarnMarkUp(ResString.readingInfo);
mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath); mediaInfos = await MediainfoUtil.ReadInfoAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, result!.ActualFilePath);
mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp())); mediaInfos.ForEach(info => Logger.InfoMarkUp(info.ToStringMarkUp()));
@ -322,27 +317,27 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//开始下载 // 开始下载
var options = new ParallelOptions() var options = new ParallelOptions()
{ {
MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount MaxDegreeOfParallelism = DownloaderConfig.MyOptions.ThreadCount
}; };
await Parallel.ForEachAsync(segments, options, async (seg, _) => await Parallel.ForEachAsync(segments, options, async (seg, _) =>
{ {
//获取文件名 // 获取文件名
var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]); var filename = GetSegmentName(seg, allHasDatetime, SamePathDic[task.Id]);
var index = seg.Index; var index = seg.Index;
var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp"); var path = Path.Combine(tmpDir, filename + $".{streamSpec.Extension ?? "clip"}.tmp");
var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers); var result = await Downloader.DownloadSegmentAsync(seg, path, speedContainer, headers);
FileDic[seg] = result; FileDic[seg] = result;
if (result != null && result.Success) if (result is { Success: true })
task.Increment(1); task.Increment(1);
//实时解密 // 实时解密
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && result != null && result.Success && !string.IsNullOrEmpty(currentKID)) if (seg.IsEncrypted && DownloaderConfig.MyOptions.MP4RealTimeDecryption && result is { Success: true } && !string.IsNullOrEmpty(currentKID))
{ {
var enc = result.ActualFilePath; var enc = result.ActualFilePath;
var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc)); var dec = Path.Combine(Path.GetDirectoryName(enc)!, Path.GetFileNameWithoutExtension(enc) + "_dec" + Path.GetExtension(enc));
var dResult = await MP4DecryptUtil.DecryptAsync(DownloaderConfig.MyOptions.UseShakaPackager, mp4decrypt, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile); var dResult = await MP4DecryptUtil.DecryptAsync(decryptEngine, decryptionBinaryPath, DownloaderConfig.MyOptions.Keys, enc, dec, currentKID, mp4InitFile);
if (dResult) if (dResult)
{ {
File.Delete(enc); File.Delete(enc);
@ -351,15 +346,14 @@ namespace N_m3u8DL_RE.DownloadManager
} }
}); });
//自动修复VTT raw字幕 // 自动修复VTT raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("vtt"))
&& streamSpec.Extension != null && streamSpec.Extension.Contains("vtt"))
{ {
//排序字幕并修正时间戳 // 排序字幕并修正时间戳
var keys = FileDic.Keys.OrderBy(k => k.Index); var keys = FileDic.Keys.OrderBy(k => k.Index).ToList();
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vttContent = File.ReadAllText(FileDic[seg]!.ActualFilePath); var vttContent = await File.ReadAllTextAsync(FileDic[seg]!.ActualFilePath);
var waitCount = 0; var waitCount = 0;
while (DownloaderConfig.MyOptions.LiveFixVttByAudio && audioStart == null && waitCount++ < 5) while (DownloaderConfig.MyOptions.LiveFixVttByAudio && audioStart == null && waitCount++ < 5)
{ {
@ -367,7 +361,7 @@ namespace N_m3u8DL_RE.DownloadManager
} }
var subOffset = audioStart != null ? (long)audioStart.Value.TotalMilliseconds : 0L; var subOffset = audioStart != null ? (long)audioStart.Value.TotalMilliseconds : 0L;
var vtt = WebVttSub.Parse(vttContent, subOffset); var vtt = WebVttSub.Parse(vttContent, subOffset);
//手动计算MPEGTS // 手动计算MPEGTS
if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration);
@ -377,11 +371,11 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//自动修复VTT mp4字幕 // 自动修复VTT mp4字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES
&& streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")) && streamSpec.Codecs != "stpp" && streamSpec.Extension != null && streamSpec.Extension.Contains("m4s"))
{ {
var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); var initFile = FileDic.Values.FirstOrDefault(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init"));
var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes); var (sawVtt, timescale) = MP4VttUtil.CheckInit(iniFileBytes);
if (sawVtt) if (sawVtt)
@ -400,11 +394,10 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//自动修复TTML raw字幕 // 自动修复TTML raw字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("ttml"))
&& streamSpec.Extension != null && streamSpec.Extension.Contains("ttml"))
{ {
var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key).ToList();
if (firstSub) if (firstSub)
{ {
if (baseTimestamp != 0) if (baseTimestamp != 0)
@ -416,7 +409,7 @@ namespace N_m3u8DL_RE.DownloadManager
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);
//手动计算MPEGTS // 手动计算MPEGTS
if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration);
@ -431,7 +424,7 @@ namespace N_m3u8DL_RE.DownloadManager
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); var vtt = MP4TtmlUtil.ExtractFromTTML(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);
//手动计算MPEGTS // 手动计算MPEGTS
if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
@ -441,15 +434,14 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//自动修复TTML mp4字幕 // 自动修复TTML mp4字幕
if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec.MediaType == Common.Enum.MediaType.SUBTITLES if (DownloaderConfig.MyOptions.AutoSubtitleFix && streamSpec is { MediaType: Common.Enum.MediaType.SUBTITLES, Extension: not null } && streamSpec.Extension.Contains("m4s")
&& streamSpec.Extension != null && streamSpec.Extension.Contains("m4s")
&& streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp")) && streamSpec.Codecs != null && streamSpec.Codecs.Contains("stpp"))
{ {
//sawTtml暂时不判断 // sawTtml暂时不判断
//var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault(); // var initFile = FileDic.Values.Where(v => Path.GetFileName(v!.ActualFilePath).StartsWith("_init")).FirstOrDefault();
//var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath); // var iniFileBytes = File.ReadAllBytes(initFile!.ActualFilePath);
//var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes); // var sawTtml = MP4TtmlUtil.CheckInit(iniFileBytes);
var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key); var keys = FileDic.OrderBy(s => s.Key.Index).Where(v => v.Value!.ActualFilePath.EndsWith(".m4s")).Select(s => s.Key);
if (firstSub) if (firstSub)
{ {
@ -462,7 +454,7 @@ namespace N_m3u8DL_RE.DownloadManager
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);
//手动计算MPEGTS // 手动计算MPEGTS
if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration); vtt.MpegtsTimestamp = 90000 * (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration);
@ -477,7 +469,7 @@ namespace N_m3u8DL_RE.DownloadManager
foreach (var seg in keys) foreach (var seg in keys)
{ {
var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp); var vtt = MP4TtmlUtil.ExtractFromMp4(FileDic[seg]!.ActualFilePath, 0, baseTimestamp);
//手动计算MPEGTS // 手动计算MPEGTS
if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0) if (currentVtt.MpegtsTimestamp == 0 && vtt.MpegtsTimestamp == 0)
{ {
vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration)); vtt.MpegtsTimestamp = 90000 * (RecordedDurDic[task.Id] + (long)keys.Where(s => s.Index < seg.Index).Sum(s => s.Duration));
@ -489,7 +481,7 @@ namespace N_m3u8DL_RE.DownloadManager
RecordedDurDic[task.Id] += (int)segmentsDuration; RecordedDurDic[task.Id] += (int)segmentsDuration;
/*//写出m3u8 /*// 写出m3u8
if (DownloaderConfig.MyOptions.LiveWriteHLS) if (DownloaderConfig.MyOptions.LiveWriteHLS)
{ {
var _saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; var _saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
@ -497,33 +489,32 @@ namespace N_m3u8DL_RE.DownloadManager
await StreamingUtil.WriteStreamListAsync(FileDic, task.Id, 0, _saveName, _saveDir); await StreamingUtil.WriteStreamListAsync(FileDic, task.Id, 0, _saveName, _saveDir);
}*/ }*/
//合并逻辑 // 合并逻辑
if (DownloaderConfig.MyOptions.LiveRealTimeMerge) if (DownloaderConfig.MyOptions.LiveRealTimeMerge)
{ {
//合并 // 合并
var outputExt = "." + streamSpec.Extension; var outputExt = "." + streamSpec.Extension;
if (streamSpec.Extension == null) outputExt = ".ts"; if (streamSpec.Extension == null) outputExt = ".ts";
else if (streamSpec.MediaType == MediaType.AUDIO && streamSpec.Extension == "m4s") outputExt = ".m4a"; else if (streamSpec is { MediaType: MediaType.AUDIO, Extension: "m4s" }) outputExt = ".m4a";
else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension == "m4s") outputExt = ".mp4"; else if (streamSpec.MediaType != MediaType.SUBTITLES && streamSpec.Extension == "m4s") outputExt = ".mp4";
else if (streamSpec.MediaType == MediaType.SUBTITLES) else if (streamSpec.MediaType == MediaType.SUBTITLES)
{ {
if (DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT) outputExt = ".srt"; outputExt = DownloaderConfig.MyOptions.SubtitleFormat == Enum.SubtitleFormat.SRT ? ".srt" : ".vtt";
else outputExt = ".vtt";
} }
var output = Path.Combine(saveDir, saveName + outputExt); var output = Path.Combine(saveDir, saveName + outputExt);
//移除无效片段 // 移除无效片段
var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key); var badKeys = FileDic.Where(i => i.Value == null).Select(i => i.Key);
foreach (var badKey in badKeys) foreach (var badKey in badKeys)
{ {
FileDic!.Remove(badKey, out _); FileDic!.Remove(badKey, out _);
} }
//设置输出流 // 设置输出流
if (fileOutputStream == null) if (fileOutputStream == null)
{ {
//检测目标文件是否存在 // 检测目标文件是否存在
while (File.Exists(output)) while (File.Exists(output))
{ {
Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}");
@ -535,20 +526,20 @@ namespace N_m3u8DL_RE.DownloadManager
} }
else else
{ {
//创建管道 // 创建管道
output = Path.ChangeExtension(output, ".ts"); output = Path.ChangeExtension(output, ".ts");
var pipeName = $"RE_pipe_{Guid.NewGuid()}"; var pipeName = $"RE_pipe_{Guid.NewGuid()}";
fileOutputStream = PipeUtil.CreatePipe(pipeName); fileOutputStream = PipeUtil.CreatePipe(pipeName);
Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]"); Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]");
PipeSteamNamesDic[task.Id] = pipeName; PipeSteamNamesDic[task.Id] = pipeName;
if (PipeSteamNamesDic.Count == SelectedSteams.Where(x => x.MediaType != MediaType.SUBTITLES).Count()) if (PipeSteamNamesDic.Count == SelectedSteams.Count(x => x.MediaType != MediaType.SUBTITLES))
{ {
var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray(); var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray();
Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]"); Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]");
var t = PipeUtil.StartPipeMuxAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, names, output); var t = PipeUtil.StartPipeMuxAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, names, output);
} }
//Windows only // Windows only
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
await (fileOutputStream as NamedPipeServerStream)!.WaitForConnectionAsync(); await (fileOutputStream as NamedPipeServerStream)!.WaitForConnectionAsync();
} }
@ -560,10 +551,10 @@ namespace N_m3u8DL_RE.DownloadManager
var files = FileDic.Where(f => f.Key != streamSpec.Playlist!.MediaInit).OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray(); var files = FileDic.Where(f => f.Key != streamSpec.Playlist!.MediaInit).OrderBy(s => s.Key.Index).Select(f => f.Value).Select(v => v!.ActualFilePath).ToArray();
if (initResult != null && mp4InitFile != "") if (initResult != null && mp4InitFile != "")
{ {
//shaka实时解密不需要init文件用于合并mp4decrpyt需要 // shaka/ffmpeg实时解密不需要init文件用于合并mp4decrpyt需要
if (!DownloaderConfig.MyOptions.UseShakaPackager) if (string.IsNullOrEmpty(currentKID) || decryptEngine == DecryptEngine.MP4DECRYPT)
{ {
files = new string[] { initResult.ActualFilePath }.Concat(files).ToArray(); files = [initResult.ActualFilePath, ..files];
} }
} }
foreach (var inputFilePath in files) foreach (var inputFilePath in files)
@ -598,7 +589,7 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//处理图形字幕 // 处理图形字幕
await SubtitleUtil.TryWriteImagePngsAsync(currentVtt, tmpDir); await SubtitleUtil.TryWriteImagePngsAsync(currentVtt, tmpDir);
var subText = currentVtt.ToVtt(); var subText = currentVtt.ToVtt();
@ -616,7 +607,7 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
//刷新buffer // 刷新buffer
if (fileOutputStream != null) if (fileOutputStream != null)
{ {
fileOutputStream.Flush(); fileOutputStream.Flush();
@ -627,11 +618,11 @@ namespace N_m3u8DL_RE.DownloadManager
break; break;
} }
if (fileOutputStream != null) if (fileOutputStream == null) return true;
{
if (!DownloaderConfig.MyOptions.LivePipeMux) if (!DownloaderConfig.MyOptions.LivePipeMux)
{ {
//记录所有文件信息 // 记录所有文件信息
OutputFiles.Add(new OutputFile() OutputFiles.Add(new OutputFile()
{ {
Index = task.Id, Index = task.Id,
@ -644,7 +635,6 @@ namespace N_m3u8DL_RE.DownloadManager
} }
fileOutputStream.Close(); fileOutputStream.Close();
fileOutputStream.Dispose(); fileOutputStream.Dispose();
}
return true; return true;
} }
@ -653,17 +643,16 @@ namespace N_m3u8DL_RE.DownloadManager
{ {
while (!STOP_FLAG) while (!STOP_FLAG)
{ {
if (WAIT_SEC != 0) if (WAIT_SEC == 0) continue;
{
//1. MPD 所有URL相同 单次请求即可获得所有轨道的信息
//2. M3U8 所有URL不同 才需要多次请求
// 1. MPD 所有URL相同 单次请求即可获得所有轨道的信息
// 2. M3U8 所有URL不同 才需要多次请求
await Parallel.ForEachAsync(dic, async (dic, _) => await Parallel.ForEachAsync(dic, async (dic, _) =>
{ {
var streamSpec = dic.Key; var streamSpec = dic.Key;
var task = dic.Value; var task = dic.Value;
//达到上限时 不需要刷新了 // 达到上限时 不需要刷新了
if (RecordLimitReachedDic[task.Id]) if (RecordLimitReachedDic[task.Id])
return; return;
@ -674,20 +663,20 @@ namespace N_m3u8DL_RE.DownloadManager
var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1; var allSamePath = allName.Count() > 1 && allName.Distinct().Count() == 1;
SamePathDic[task.Id] = allSamePath; SamePathDic[task.Id] = allSamePath;
} }
//过滤不需要下载的片段 // 过滤不需要下载的片段
FilterMediaSegments(streamSpec, task, allHasDatetime, SamePathDic[task.Id]); FilterMediaSegments(streamSpec, task, allHasDatetime, SamePathDic[task.Id]);
var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments; var newList = streamSpec.Playlist!.MediaParts[0].MediaSegments;
if (newList.Count > 0) if (newList.Count > 0)
{ {
task.MaxValue += newList.Count; task.MaxValue += newList.Count;
//推送给消费者 // 推送给消费者
await BlockDic[task.Id].SendAsync(newList); await BlockDic[task.Id].SendAsync(newList);
//更新最新链接 // 更新最新链接
LastFileNameDic[task.Id] = GetSegmentName(newList.Last(), allHasDatetime, SamePathDic[task.Id]); LastFileNameDic[task.Id] = GetSegmentName(newList.Last(), allHasDatetime, SamePathDic[task.Id]);
//尝试更新时间戳 // 尝试更新时间戳
var dt = newList.Last().DateTime; var dt = newList.Last().DateTime;
DateTimeDic[task.Id] = dt != null ? GetUnixTimestamp(dt.Value) : 0L; DateTimeDic[task.Id] = dt != null ? GetUnixTimestamp(dt.Value) : 0L;
//累加已获取到的时长 // 累加已获取到的时长
RefreshedDurDic[task.Id] += (int)newList.Sum(s => s.Duration); RefreshedDurDic[task.Id] += (int)newList.Sum(s => s.Duration);
} }
@ -696,8 +685,8 @@ namespace N_m3u8DL_RE.DownloadManager
RecordLimitReachedDic[task.Id] = true; RecordLimitReachedDic[task.Id] = true;
} }
//检测时长限制 // 检测时长限制
if (!STOP_FLAG && RecordLimitReachedDic.Values.All(x => x == true)) if (!STOP_FLAG && RecordLimitReachedDic.Values.All(x => x))
{ {
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
STOP_FLAG = true; STOP_FLAG = true;
@ -707,20 +696,20 @@ namespace N_m3u8DL_RE.DownloadManager
try try
{ {
//Logger.WarnMarkUp($"wait {waitSec}s"); // Logger.WarnMarkUp($"wait {waitSec}s");
if (!STOP_FLAG) await Task.Delay(WAIT_SEC * 1000, CancellationTokenSource.Token); if (!STOP_FLAG) await Task.Delay(WAIT_SEC * 1000, CancellationTokenSource.Token);
//刷新列表 // 刷新列表
if (!STOP_FLAG) await StreamExtractor.RefreshPlayListAsync(dic.Keys.ToList()); if (!STOP_FLAG) await StreamExtractor.RefreshPlayListAsync(dic.Keys.ToList());
} }
catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token) catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
{ {
//不需要做事 // 不需要做事
} }
catch (Exception e) catch (Exception e)
{ {
Logger.ErrorMarkUp(e); Logger.ErrorMarkUp(e);
STOP_FLAG = true; STOP_FLAG = true;
//停止所有Block // 停止所有Block
foreach (var target in BlockDic.Values) foreach (var target in BlockDic.Values)
{ {
target.Complete(); target.Complete();
@ -728,7 +717,6 @@ namespace N_m3u8DL_RE.DownloadManager
} }
} }
} }
}
private void FilterMediaSegments(StreamSpec streamSpec, ProgressTask task, bool allHasDatetime, bool allSamePath) private void FilterMediaSegments(StreamSpec streamSpec, ProgressTask task, bool allHasDatetime, bool allSamePath)
{ {
@ -738,7 +726,7 @@ namespace N_m3u8DL_RE.DownloadManager
var dateTime = DateTimeDic[task.Id]; var dateTime = DateTimeDic[task.Id];
var lastName = LastFileNameDic[task.Id]; var lastName = LastFileNameDic[task.Id];
//优先使用dateTime判断 // 优先使用dateTime判断
if (dateTime != 0 && streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null)) if (dateTime != 0 && streamSpec.Playlist!.MediaParts[0].MediaSegments.All(s => s.DateTime != null))
{ {
index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetUnixTimestamp(s.DateTime!.Value) == dateTime); index = streamSpec.Playlist!.MediaParts[0].MediaSegments.FindIndex(s => GetUnixTimestamp(s.DateTime!.Value) == dateTime);
@ -750,7 +738,7 @@ namespace N_m3u8DL_RE.DownloadManager
if (index > -1) if (index > -1)
{ {
//修正Index // 修正Index
var list = streamSpec.Playlist!.MediaParts[0].MediaSegments.Skip(index + 1).ToList(); var list = streamSpec.Playlist!.MediaParts[0].MediaSegments.Skip(index + 1).ToList();
if (list.Count > 0) if (list.Count > 0)
{ {
@ -773,27 +761,27 @@ namespace N_m3u8DL_RE.DownloadManager
public async Task<bool> StartRecordAsync() public async Task<bool> StartRecordAsync()
{ {
var takeLastCount = DownloaderConfig.MyOptions.LiveTakeCount; var takeLastCount = DownloaderConfig.MyOptions.LiveTakeCount;
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); //速度计算 ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算
ConcurrentDictionary<StreamSpec, bool?> Results = new(); ConcurrentDictionary<StreamSpec, bool?> Results = new();
//同步流 // 同步流
FilterUtil.SyncStreams(SelectedSteams, takeLastCount); FilterUtil.SyncStreams(SelectedSteams, takeLastCount);
//设置等待时间 // 设置等待时间
if (WAIT_SEC == 0) if (WAIT_SEC == 0)
{ {
WAIT_SEC = (int)(SelectedSteams.Min(s => s.Playlist!.MediaParts[0].MediaSegments.Sum(s => s.Duration)) / 2); WAIT_SEC = (int)(SelectedSteams.Min(s => s.Playlist!.MediaParts[0].MediaSegments.Sum(s => s.Duration)) / 2);
WAIT_SEC -= 2; //再提前两秒吧 留出冗余 WAIT_SEC -= 2; // 再提前两秒吧 留出冗余
if (DownloaderConfig.MyOptions.LiveWaitTime != null) if (DownloaderConfig.MyOptions.LiveWaitTime != null)
WAIT_SEC = DownloaderConfig.MyOptions.LiveWaitTime.Value; WAIT_SEC = DownloaderConfig.MyOptions.LiveWaitTime.Value;
if (WAIT_SEC <= 0) WAIT_SEC = 1; if (WAIT_SEC <= 0) WAIT_SEC = 1;
Logger.WarnMarkUp($"set refresh interval to {WAIT_SEC} seconds"); Logger.WarnMarkUp($"set refresh interval to {WAIT_SEC} seconds");
} }
//如果没有选中音频 取消通过音频修复vtt时间轴 // 如果没有选中音频 取消通过音频修复vtt时间轴
if (!SelectedSteams.Any(x => x.MediaType == MediaType.AUDIO)) if (SelectedSteams.All(x => x.MediaType != MediaType.AUDIO))
{ {
DownloaderConfig.MyOptions.LiveFixVttByAudio = false; DownloaderConfig.MyOptions.LiveFixVttByAudio = false;
} }
/*//写出master /*// 写出master
if (DownloaderConfig.MyOptions.LiveWriteHLS) if (DownloaderConfig.MyOptions.LiveWriteHLS)
{ {
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
@ -804,14 +792,14 @@ namespace N_m3u8DL_RE.DownloadManager
var progress = CustomAnsiConsole.Console.Progress().AutoClear(true); var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF; progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;
//进度条的列定义 // 进度条的列定义
var progressColumns = new ProgressColumn[] var progressColumns = new ProgressColumn[]
{ {
new TaskDescriptionColumn() { Alignment = Justify.Left }, new TaskDescriptionColumn() { Alignment = Justify.Left },
new RecordingDurationColumn(RecordedDurDic, RefreshedDurDic), //时长显示 new RecordingDurationColumn(RecordedDurDic, RefreshedDurDic), // 时长显示
new RecordingStatusColumn(), new RecordingStatusColumn(),
new PercentageColumn(), new PercentageColumn(),
new DownloadSpeedColumn(SpeedContainerDic), //速度计算 new DownloadSpeedColumn(SpeedContainerDic), // 速度计算
new SpinnerColumn(), new SpinnerColumn(),
}; };
if (DownloaderConfig.MyOptions.NoAnsiColor) if (DownloaderConfig.MyOptions.NoAnsiColor)
@ -822,12 +810,12 @@ namespace N_m3u8DL_RE.DownloadManager
await progress.StartAsync(async ctx => await progress.StartAsync(async ctx =>
{ {
//创建任务 // 创建任务
var dic = SelectedSteams.Select(item => var dic = SelectedSteams.Select(item =>
{ {
var task = ctx.AddTask(item.ToShortShortString(), autoStart: false, maxValue: 0); var task = ctx.AddTask(item.ToShortShortString(), autoStart: false, maxValue: 0);
SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算 SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算
//限速设置 // 限速设置
if (DownloaderConfig.MyOptions.MaxSpeed != null) if (DownloaderConfig.MyOptions.MaxSpeed != null)
{ {
SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value; SpeedContainerDic[task.Id].SpeedLimit = DownloaderConfig.MyOptions.MaxSpeed.Value;
@ -837,29 +825,28 @@ namespace N_m3u8DL_RE.DownloadManager
DateTimeDic[task.Id] = 0L; DateTimeDic[task.Id] = 0L;
RecordedDurDic[task.Id] = 0; RecordedDurDic[task.Id] = 0;
RefreshedDurDic[task.Id] = 0; RefreshedDurDic[task.Id] = 0;
MaxIndexDic[task.Id] = item.Playlist?.MediaParts[0].MediaSegments.LastOrDefault()?.Index ?? 0L; //最大Index MaxIndexDic[task.Id] = item.Playlist?.MediaParts[0].MediaSegments.LastOrDefault()?.Index ?? 0L; // 最大Index
BlockDic[task.Id] = new BufferBlock<List<MediaSegment>>(); BlockDic[task.Id] = new BufferBlock<List<MediaSegment>>();
return (item, task); return (item, task);
}).ToDictionary(item => item.item, item => item.task); }).ToDictionary(item => item.item, item => item.task);
DownloaderConfig.MyOptions.ConcurrentDownload = true; DownloaderConfig.MyOptions.ConcurrentDownload = true;
DownloaderConfig.MyOptions.MP4RealTimeDecryption = true; DownloaderConfig.MyOptions.MP4RealTimeDecryption = true;
DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue; DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;
if (DownloaderConfig.MyOptions.MP4RealTimeDecryption && !DownloaderConfig.MyOptions.UseShakaPackager if (DownloaderConfig.MyOptions is { MP4RealTimeDecryption: true, DecryptionEngine: not DecryptEngine.SHAKA_PACKAGER, Keys.Length: > 0 })
&& DownloaderConfig.MyOptions.Keys != null && DownloaderConfig.MyOptions.Keys.Length > 0)
Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.realTimeDecMessage}[/]");
var limit = DownloaderConfig.MyOptions.LiveRecordLimit; var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
if (limit != TimeSpan.MaxValue) if (limit != TimeSpan.MaxValue)
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");
//录制直播时,用户选了几个流就并发录几个 // 录制直播时,用户选了几个流就并发录几个
var options = new ParallelOptions() var options = new ParallelOptions()
{ {
MaxDegreeOfParallelism = SelectedSteams.Count MaxDegreeOfParallelism = SelectedSteams.Count
}; };
//开始刷新 // 开始刷新
var producerTask = PlayListProduceAsync(dic); var producerTask = PlayListProduceAsync(dic);
await Task.Delay(200); await Task.Delay(200);
//并发下载 // 并发下载
await Parallel.ForEachAsync(dic, options, async (kp, _) => await Parallel.ForEachAsync(dic, options, async (kp, _) =>
{ {
var task = kp.Value; var task = kp.Value;
@ -870,8 +857,8 @@ namespace N_m3u8DL_RE.DownloadManager
var success = Results.Values.All(v => v == true); var success = Results.Values.All(v => v == true);
//删除临时文件夹 // 删除临时文件夹
if (!DownloaderConfig.MyOptions.SkipMerge && DownloaderConfig.MyOptions.DelAfterDone && success) if (DownloaderConfig.MyOptions is { SkipMerge: false, DelAfterDone: true } && success)
{ {
foreach (var item in StreamExtractor.RawFiles) foreach (var item in StreamExtractor.RawFiles)
{ {
@ -881,12 +868,12 @@ namespace N_m3u8DL_RE.DownloadManager
OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix); OtherUtil.SafeDeleteDir(DownloaderConfig.DirPrefix);
} }
//混流 // 混流
if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0) if (success && DownloaderConfig.MyOptions.MuxAfterDone && OutputFiles.Count > 0)
{ {
OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList(); OutputFiles = OutputFiles.OrderBy(o => o.Index).ToList();
//是否跳过字幕 // 是否跳过字幕
if (DownloaderConfig.MyOptions.MuxOptions.SkipSubtitle) if (DownloaderConfig.MyOptions.MuxOptions!.SkipSubtitle)
{ {
OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList(); OutputFiles = OutputFiles.Where(o => o.MediaType != MediaType.SUBTITLES).ToList();
} }
@ -896,15 +883,15 @@ namespace N_m3u8DL_RE.DownloadManager
} }
OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]")); OutputFiles.ForEach(f => Logger.WarnMarkUp($"[grey]{Path.GetFileName(f.FilePath).EscapeMarkup()}[/]"));
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory; var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
var ext = DownloaderConfig.MyOptions.MuxOptions.MuxToMp4 ? ".mp4" : ".mkv"; var ext = OtherUtil.GetMuxExtension(DownloaderConfig.MyOptions.MuxOptions.MuxFormat);
var dirName = Path.GetFileName(DownloaderConfig.DirPrefix); var dirName = Path.GetFileName(DownloaderConfig.DirPrefix);
var outName = $"{dirName}.MUX"; var outName = $"{dirName}.MUX";
var outPath = Path.Combine(saveDir, outName); var outPath = Path.Combine(saveDir, outName);
Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]"); Logger.WarnMarkUp($"Muxing to [grey]{outName.EscapeMarkup()}{ext}[/]");
var result = false; var result = false;
if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath); if (DownloaderConfig.MyOptions.MuxOptions.UseMkvmerge) result = MergeUtil.MuxInputsByMkvmerge(DownloaderConfig.MyOptions.MkvmergeBinaryPath!, OutputFiles.ToArray(), outPath);
else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxToMp4, !DownloaderConfig.MyOptions.NoDateInfo); else result = MergeUtil.MuxInputsByFFmpeg(DownloaderConfig.MyOptions.FFmpegBinaryPath!, OutputFiles.ToArray(), outPath, DownloaderConfig.MyOptions.MuxOptions.MuxFormat, !DownloaderConfig.MyOptions.NoDateInfo);
//完成后删除各轨道文件 // 完成后删除各轨道文件
if (result) if (result)
{ {
if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles) if (!DownloaderConfig.MyOptions.MuxOptions.KeepFiles)
@ -920,7 +907,7 @@ namespace N_m3u8DL_RE.DownloadManager
success = false; success = false;
Logger.ErrorMarkUp($"Mux failed"); Logger.ErrorMarkUp($"Mux failed");
} }
//判断是否要改名 // 判断是否要改名
var newPath = Path.ChangeExtension(outPath, ext); var newPath = Path.ChangeExtension(outPath, ext);
if (result && !File.Exists(newPath)) if (result && !File.Exists(newPath))
{ {
@ -931,5 +918,4 @@ namespace N_m3u8DL_RE.DownloadManager
return success; return success;
} }
}
} }

View File

@ -1,15 +1,9 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Downloader namespace N_m3u8DL_RE.Downloader;
internal interface IDownloader
{ {
internal interface IDownloader
{
Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null); Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null);
}
} }

View File

@ -6,23 +6,14 @@ using N_m3u8DL_RE.Crypto;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using N_m3u8DL_RE.Util; using N_m3u8DL_RE.Util;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Downloader namespace N_m3u8DL_RE.Downloader;
/// <summary>
/// 简单下载器
/// </summary>
internal class SimpleDownloader : IDownloader
{ {
/// <summary>
/// 简单下载器
/// </summary>
internal class SimpleDownloader : IDownloader
{
DownloaderConfig DownloaderConfig; DownloaderConfig DownloaderConfig;
public SimpleDownloader(DownloaderConfig config) public SimpleDownloader(DownloaderConfig config)
@ -34,23 +25,25 @@ namespace N_m3u8DL_RE.Downloader
{ {
var url = segment.Url; var url = segment.Url;
var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount); var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount);
if (dResult != null && dResult.Success && dResult.ActualFilePath != des) if (dResult is { Success: true } && dResult.ActualFilePath != des)
{ {
if (segment.EncryptInfo != null) switch (segment.EncryptInfo.Method)
{ {
if (segment.EncryptInfo.Method == EncryptMethod.AES_128) case EncryptMethod.AES_128:
{ {
var key = segment.EncryptInfo.Key; var key = segment.EncryptInfo.Key;
var iv = segment.EncryptInfo.IV; var iv = segment.EncryptInfo.IV;
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!); AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!);
break;
} }
else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB) case EncryptMethod.AES_128_ECB:
{ {
var key = segment.EncryptInfo.Key; var key = segment.EncryptInfo.Key;
var iv = segment.EncryptInfo.IV; var iv = segment.EncryptInfo.IV;
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB); AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB);
break;
} }
else if (segment.EncryptInfo.Method == EncryptMethod.CHACHA20) case EncryptMethod.CHACHA20:
{ {
var key = segment.EncryptInfo.Key; var key = segment.EncryptInfo.Key;
var nonce = segment.EncryptInfo.IV; var nonce = segment.EncryptInfo.IV;
@ -58,25 +51,25 @@ namespace N_m3u8DL_RE.Downloader
var fileBytes = File.ReadAllBytes(dResult.ActualFilePath); var fileBytes = File.ReadAllBytes(dResult.ActualFilePath);
var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!); var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!);
await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted); await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted);
break;
} }
else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR) case EncryptMethod.SAMPLE_AES_CTR:
{ // throw new NotSupportedException("SAMPLE-AES-CTR");
//throw new NotSupportedException("SAMPLE-AES-CTR"); break;
} }
//Image头处理 // Image头处理
if (dResult.ImageHeader) if (dResult.ImageHeader)
{ {
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath); await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
} }
//Gzip解压 // Gzip解压
if (dResult.GzipHeader) if (dResult.GzipHeader)
{ {
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath); await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
} }
}
//处理完成后改名 // 处理完成后改名
File.Move(dResult.ActualFilePath, des); File.Move(dResult.ActualFilePath, des);
dResult.ActualFilePath = des; dResult.ActualFilePath = des;
} }
@ -92,14 +85,14 @@ namespace N_m3u8DL_RE.Downloader
cancellationTokenSource = new(); cancellationTokenSource = new();
var des = Path.ChangeExtension(path, null); var des = Path.ChangeExtension(path, null);
//已下载跳过 // 已下载跳过
if (File.Exists(des)) if (File.Exists(des))
{ {
speedContainer.Add(new FileInfo(des).Length); speedContainer.Add(new FileInfo(des).Length);
return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des }); return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des });
} }
//已解密跳过 // 已解密跳过
var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + "_dec" + Path.GetExtension(des)); var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + "_dec" + Path.GetExtension(des));
if (File.Exists(dec)) if (File.Exists(dec))
{ {
@ -107,15 +100,16 @@ namespace N_m3u8DL_RE.Downloader
return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec }); return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec });
} }
//另起线程进行监控 // 另起线程进行监控
var cts = cancellationTokenSource;
using var watcher = Task.Factory.StartNew(async () => using var watcher = Task.Factory.StartNew(async () =>
{ {
while (true) while (true)
{ {
if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break; if (cts.IsCancellationRequested) break;
if (speedContainer.ShouldStop) if (speedContainer.ShouldStop)
{ {
cancellationTokenSource.Cancel(); cts.Cancel();
Logger.DebugMarkUp("Cancel..."); Logger.DebugMarkUp("Cancel...");
break; break;
} }
@ -123,7 +117,7 @@ namespace N_m3u8DL_RE.Downloader
} }
}); });
//调用下载 // 调用下载
var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition); var result = await DownloadUtil.DownloadToFileAsync(url, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition);
return (des, result); return (des, result);
@ -132,7 +126,7 @@ namespace N_m3u8DL_RE.Downloader
catch (Exception ex) catch (Exception ex)
{ {
Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]"); Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
Logger.Debug(url + " " + ex.ToString()); Logger.Debug(url + " " + ex);
Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}"); Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
if (retryCount-- > 0) if (retryCount-- > 0)
{ {
@ -144,18 +138,17 @@ namespace N_m3u8DL_RE.Downloader
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}"); 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}");
Logger.WarnMarkUp($"[grey]{ex.Message.EscapeMarkup()}[/]"); Logger.WarnMarkUp($"[grey]{ex.Message.EscapeMarkup()}[/]");
} }
//throw new Exception("download failed", ex); // throw new Exception("download failed", ex);
return default; return default;
} }
finally finally
{ {
if (cancellationTokenSource != null) if (cancellationTokenSource != null)
{ {
//调用后销毁 // 调用后销毁
cancellationTokenSource.Dispose(); cancellationTokenSource.Dispose();
cancellationTokenSource = null; cancellationTokenSource = null;
} }
} }
} }
}
} }

View File

@ -1,13 +1,7 @@
using System; namespace N_m3u8DL_RE.Entity;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity public class CustomRange
{ {
public class CustomRange
{
public required string InputStr { get; set; } public required string InputStr { get; set; }
public double? StartSec { get; set; } public double? StartSec { get; set; }
public double? EndSec { get; set; } public double? EndSec { get; set; }
@ -19,5 +13,4 @@ namespace N_m3u8DL_RE.Entity
{ {
return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}"; return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}";
} }
}
} }

View File

@ -1,19 +1,11 @@
using System; namespace N_m3u8DL_RE.Entity;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity internal class DownloadResult
{ {
internal class DownloadResult public bool Success => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength != null);
{
public bool Success { get => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength == null ? false : true); }
public long? RespContentLength { get; set; } public long? RespContentLength { get; set; }
public long? ActualContentLength { get; set; } public long? ActualContentLength { get; set; }
public bool ImageHeader { get; set; } = false; //图片伪装 public bool ImageHeader { get; set; } = false; // 图片伪装
public bool GzipHeader { get; set; } = false; //GZip压缩 public bool GzipHeader { get; set; } = false; // GZip压缩
public required string ActualFilePath { get; set; } public required string ActualFilePath { get; set; }
}
} }

View File

@ -1,14 +1,9 @@
using System; using Spectre.Console;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Spectre.Console;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
internal class Mediainfo
{ {
internal class Mediainfo
{
public string? Id { get; set; } public string? Id { get; set; }
public string? Text { get; set; } public string? Text { get; set; }
public string? BaseInfo { get; set; } public string? BaseInfo { get; set; }
@ -29,5 +24,4 @@ namespace N_m3u8DL_RE.Entity
{ {
return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]"; return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]";
} }
}
} }

View File

@ -1,17 +1,12 @@
using System; using N_m3u8DL_RE.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
internal class MuxOptions
{ {
internal class MuxOptions
{
public bool UseMkvmerge { get; set; } = false; public bool UseMkvmerge { get; set; } = false;
public bool MuxToMp4 { get; set; } = false; public MuxFormat MuxFormat { get; set; } = MuxFormat.MP4;
public bool KeepFiles { get; set; } = false; public bool KeepFiles { get; set; } = false;
public bool SkipSubtitle { get; set; } = false; public bool SkipSubtitle { get; set; } = false;
public string? BinPath { get; set; } public string? BinPath { get; set; }
}
} }

View File

@ -1,19 +1,13 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
internal class OutputFile
{ {
internal class OutputFile
{
public MediaType? MediaType { get; set; } public MediaType? MediaType { get; set; }
public required int Index { get; set; } public required int Index { get; set; }
public required string FilePath { get; set; } public required string FilePath { get; set; }
public string? LangCode { get; set; } public string? LangCode { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public List<Mediainfo> Mediainfos { get; set; } = new(); public List<Mediainfo> Mediainfos { get; set; } = [];
}
} }

View File

@ -1,30 +1,21 @@
using NiL.JS.Statements; namespace N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity internal class SpeedContainer
{ {
internal class SpeedContainer
{
public bool SingleSegment { get; set; } = false; public bool SingleSegment { get; set; } = false;
public long NowSpeed { get; set; } = 0L; //当前每秒速度 public long NowSpeed { get; set; } = 0L; // 当前每秒速度
public long SpeedLimit { get; set; } = long.MaxValue; //限速设置 public long SpeedLimit { get; set; } = long.MaxValue; // 限速设置
public long? ResponseLength { get; set; } public long? ResponseLength { get; set; }
public long RDownloaded { get => _Rdownloaded; } public long RDownloaded => _Rdownloaded;
private int _zeroSpeedCount = 0; private int _zeroSpeedCount = 0;
public int LowSpeedCount { get => _zeroSpeedCount; } public int LowSpeedCount => _zeroSpeedCount;
public bool ShouldStop { get => LowSpeedCount >= 20; } public bool ShouldStop => LowSpeedCount >= 20;
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
private long _downloaded = 0; private long _downloaded = 0;
private long _Rdownloaded = 0; private long _Rdownloaded = 0;
public long Downloaded { get => _downloaded; } public long Downloaded => _downloaded;
public int AddLowSpeedCount() public int AddLowSpeedCount()
{ {
@ -55,5 +46,4 @@ namespace N_m3u8DL_RE.Entity
ResponseLength = null; ResponseLength = null;
_Rdownloaded = 0L; _Rdownloaded = 0L;
} }
}
} }

View File

@ -1,15 +1,11 @@
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Entity namespace N_m3u8DL_RE.Entity;
public class StreamFilter
{ {
public class StreamFilter
{
public Regex? GroupIdReg { get; set; } public Regex? GroupIdReg { get; set; }
public Regex? LanguageReg { get; set; } public Regex? LanguageReg { get; set; }
public Regex? NameReg { get; set; } public Regex? NameReg { get; set; }
@ -50,7 +46,6 @@ namespace N_m3u8DL_RE.Entity
if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} "); if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} ");
if (Role.HasValue) sb.Append($"Role: {Role} "); if (Role.HasValue) sb.Append($"Role: {Role} ");
return sb.ToString() + $"For: {For}"; return sb + $"For: {For}";
}
} }
} }

View File

@ -0,0 +1,8 @@
namespace N_m3u8DL_RE.Enum;
internal enum DecryptEngine
{
MP4DECRYPT,
SHAKA_PACKAGER,
FFMPEG,
}

View File

@ -0,0 +1,8 @@
namespace N_m3u8DL_RE.Enum;
internal enum MuxFormat
{
MP4,
MKV,
TS,
}

View File

@ -1,14 +1,7 @@
using System; namespace N_m3u8DL_RE.Enum;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Enum internal enum SubtitleFormat
{ {
internal enum SubtitleFormat
{
VTT, VTT,
SRT SRT
}
} }

View File

@ -2,12 +2,12 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RootNamespace>N_m3u8DL_RE</RootNamespace> <RootNamespace>N_m3u8DL_RE</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.2.1</Version> <Version>0.3.0</Version>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
</PropertyGroup> </PropertyGroup>
@ -16,7 +16,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NiL.JS" Version="2.5.1600" /> <PackageReference Include="NiL.JS" Version="2.5.1684" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
</ItemGroup> </ItemGroup>

View File

@ -2,16 +2,11 @@
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Processor; using N_m3u8DL_RE.Parser.Processor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Processor namespace N_m3u8DL_RE.Processor;
internal class DemoProcessor : ContentProcessor
{ {
internal class DemoProcessor : ContentProcessor
{
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig)
{ {
@ -23,5 +18,4 @@ namespace N_m3u8DL_RE.Processor
Logger.InfoMarkUp("[red]Match bitmovin![/]"); Logger.InfoMarkUp("[red]Match bitmovin![/]");
return rawText; return rawText;
} }
}
} }

View File

@ -5,16 +5,11 @@ using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Parser.Processor; using N_m3u8DL_RE.Parser.Processor;
using N_m3u8DL_RE.Parser.Processor.HLS; using N_m3u8DL_RE.Parser.Processor.HLS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Processor namespace N_m3u8DL_RE.Processor;
internal class DemoProcessor2 : KeyProcessor
{ {
internal class DemoProcessor2 : KeyProcessor
{
public override bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig) public override bool CanProcess(ExtractorType extractorType, string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
{ {
return extractorType == ExtractorType.HLS && parserConfig.Url.Contains("playertest.longtailvideo.com"); return extractorType == ExtractorType.HLS && parserConfig.Url.Contains("playertest.longtailvideo.com");
@ -27,5 +22,4 @@ namespace N_m3u8DL_RE.Processor
Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]"); Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]");
return info; return info;
} }
}
} }

View File

@ -6,19 +6,12 @@ using N_m3u8DL_RE.Parser.Util;
using NiL.JS.BaseLibrary; using NiL.JS.BaseLibrary;
using NiL.JS.Core; using NiL.JS.Core;
using NiL.JS.Extensions; using NiL.JS.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Processor namespace N_m3u8DL_RE.Processor;
// "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"
internal class NowehoryzontyUrlProcessor : UrlProcessor
{ {
//"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"
internal class NowehoryzontyUrlProcessor : UrlProcessor
{
private static string START = "nowehoryzonty:"; private static string START = "nowehoryzonty:";
private static string? TimeDifferenceStr = null; private static string? TimeDifferenceStr = null;
private static int? TimeDifference = null; private static int? TimeDifference = null;
@ -220,5 +213,4 @@ namespace N_m3u8DL_RE.Processor
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/subtitle_pl/34.m4s','vx54axqjal4f0yy2',-2274)); //console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/subtitle_pl/34.m4s','vx54axqjal4f0yy2',-2274));
"""; """;
}
} }

View File

@ -1,11 +1,11 @@
using N_m3u8DL_RE.Parser.Config; using System.Globalization;
using N_m3u8DL_RE.Parser.Config;
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Enum;
using N_m3u8DL_RE.Parser; using N_m3u8DL_RE.Parser;
using Spectre.Console; using Spectre.Console;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using System.Globalization;
using System.Text; using System.Text;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Processor; using N_m3u8DL_RE.Processor;
@ -14,24 +14,34 @@ using N_m3u8DL_RE.Util;
using N_m3u8DL_RE.DownloadManager; using N_m3u8DL_RE.DownloadManager;
using N_m3u8DL_RE.CommandLine; using N_m3u8DL_RE.CommandLine;
using System.Net; using System.Net;
using System.Net.Http.Headers; using N_m3u8DL_RE.Enum;
namespace N_m3u8DL_RE namespace N_m3u8DL_RE;
internal class Program
{ {
internal class Program
{
static async Task Main(string[] args) static async Task Main(string[] args)
{ {
// 处理NT6.0及以下System.CommandLine报错CultureNotFound问题
if (OperatingSystem.IsWindows())
{
var osVersion = Environment.OSVersion.Version;
if (osVersion.Major < 6 || osVersion is { Major: 6, Minor: 0 })
{
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
}
}
Console.CancelKeyPress += Console_CancelKeyPress; Console.CancelKeyPress += Console_CancelKeyPress;
ServicePointManager.DefaultConnectionLimit = 1024; ServicePointManager.DefaultConnectionLimit = 1024;
try { Console.CursorVisible = true; } catch { } try { Console.CursorVisible = true; } catch { }
string loc = "en-US"; string loc = ResString.CurrentLoc;
string currLoc = Thread.CurrentThread.CurrentUICulture.Name; string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
if (currLoc == "zh-CN" || currLoc == "zh-SG") loc = "zh-CN"; if (currLoc is "zh-CN" or "zh-SG") loc = "zh-CN";
else if (currLoc.StartsWith("zh-")) loc = "zh-TW"; else if (currLoc.StartsWith("zh-")) loc = "zh-TW";
//处理用户-h等请求 // 处理用户-h等请求
var index = -1; var index = -1;
var list = new List<string>(args); var list = new List<string>(args);
if ((index = list.IndexOf("--ui-language")) != -1 && list.Count > index + 1 && new List<string> { "en-US", "zh-CN", "zh-TW" }.Contains(list[index + 1])) if ((index = list.IndexOf("--ui-language")) != -1 && list.Count > index + 1 && new List<string> { "en-US", "zh-CN", "zh-TW" }.Contains(list[index + 1]))
@ -39,10 +49,18 @@ namespace N_m3u8DL_RE
loc = list[index + 1]; loc = list[index + 1];
} }
ResString.CurrentLoc = loc;
try
{
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc); CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(loc);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(loc); Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(loc);
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(loc); Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(loc);
}
catch
{
// Culture not work on NT6.0, so catch the exception
}
await CommandInvoker.InvokeArgs(args, DoWorkAsync); await CommandInvoker.InvokeArgs(args, DoWorkAsync);
} }
@ -54,7 +72,7 @@ namespace N_m3u8DL_RE
{ {
Console.CursorVisible = true; Console.CursorVisible = true;
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
System.Diagnostics.Process.Start("stty", "echo"); System.Diagnostics.Process.Start("tput", "cnorm");
} catch { } } catch { }
Environment.Exit(0); Environment.Exit(0);
} }
@ -69,7 +87,7 @@ namespace N_m3u8DL_RE
static async Task DoWorkAsync(MyOption option) static async Task DoWorkAsync(MyOption option)
{ {
HTTPUtil.AppHttpClient.Timeout = TimeSpan.FromSeconds(option.HttpRequestTimeout);
if (Console.IsOutputRedirected || Console.IsErrorRedirected) if (Console.IsOutputRedirected || Console.IsErrorRedirected)
{ {
option.ForceAnsiConsole = true; option.ForceAnsiConsole = true;
@ -77,8 +95,10 @@ namespace N_m3u8DL_RE
Logger.Info(ResString.consoleRedirected); Logger.Info(ResString.consoleRedirected);
} }
CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor); CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor);
//检测更新
CheckUpdateAsync(); // 检测更新
if (!option.DisableUpdateCheck)
_ = CheckUpdateAsync();
Logger.IsWriteFile = !option.NoLog; Logger.IsWriteFile = !option.NoLog;
Logger.InitLogFile(); Logger.InitLogFile();
@ -96,23 +116,26 @@ namespace N_m3u8DL_RE
HTTPUtil.HttpClientHandler.UseProxy = true; HTTPUtil.HttpClientHandler.UseProxy = true;
} }
//检查互斥的选项 // 检查互斥的选项
if (option is { MuxAfterDone: false, MuxImports.Count: > 0 })
if (!option.MuxAfterDone && option.MuxImports != null && option.MuxImports.Count > 0)
{ {
throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!"); throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!");
} }
//LivePipeMux开启时 LiveRealTimeMerge必须开启 if (option.UseShakaPackager)
if (option.LivePipeMux && !option.LiveRealTimeMerge) {
option.DecryptionEngine = DecryptEngine.SHAKA_PACKAGER;
}
// LivePipeMux开启时 LiveRealTimeMerge必须开启
if (option is { LivePipeMux: true, LiveRealTimeMerge: false })
{ {
Logger.WarnMarkUp("LivePipeMux detected, forced enable LiveRealTimeMerge"); Logger.WarnMarkUp("LivePipeMux detected, forced enable LiveRealTimeMerge");
option.LiveRealTimeMerge = true; option.LiveRealTimeMerge = true;
} }
//预先检查ffmpeg // 预先检查ffmpeg
if (option.FFmpegBinaryPath == null) option.FFmpegBinaryPath ??= GlobalUtil.FindExecutable("ffmpeg");
option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg");
if (string.IsNullOrEmpty(option.FFmpegBinaryPath) || !File.Exists(option.FFmpegBinaryPath)) if (string.IsNullOrEmpty(option.FFmpegBinaryPath) || !File.Exists(option.FFmpegBinaryPath))
{ {
@ -121,53 +144,59 @@ namespace N_m3u8DL_RE
Logger.Extra($"ffmpeg => {option.FFmpegBinaryPath}"); Logger.Extra($"ffmpeg => {option.FFmpegBinaryPath}");
//预先检查mkvmerge // 预先检查mkvmerge
if (option.MuxOptions != null && option.MuxOptions.UseMkvmerge && option.MuxAfterDone) if (option is { MuxOptions.UseMkvmerge: true, MuxAfterDone: true })
{ {
if (option.MkvmergeBinaryPath == null) option.MkvmergeBinaryPath ??= GlobalUtil.FindExecutable("mkvmerge");
option.MkvmergeBinaryPath = GlobalUtil.FindExecutable("mkvmerge");
if (string.IsNullOrEmpty(option.MkvmergeBinaryPath) || !File.Exists(option.MkvmergeBinaryPath)) if (string.IsNullOrEmpty(option.MkvmergeBinaryPath) || !File.Exists(option.MkvmergeBinaryPath))
{ {
throw new FileNotFoundException("mkvmerge not found"); throw new FileNotFoundException(ResString.mkvmergeNotFound);
} }
Logger.Extra($"mkvmerge => {option.MkvmergeBinaryPath}"); Logger.Extra($"mkvmerge => {option.MkvmergeBinaryPath}");
} }
//预先检查 // 预先检查
if ((option.Keys != null && option.Keys.Length > 0) || option.KeyTextFile != null) if (option.Keys is { Length: > 0 } || option.KeyTextFile != null)
{ {
if (string.IsNullOrEmpty(option.DecryptionBinaryPath)) if (!string.IsNullOrEmpty(option.DecryptionBinaryPath) && !File.Exists(option.DecryptionBinaryPath))
{ {
if (option.UseShakaPackager) throw new FileNotFoundException(option.DecryptionBinaryPath);
}
switch (option.DecryptionEngine)
{
case DecryptEngine.SHAKA_PACKAGER:
{ {
var file = GlobalUtil.FindExecutable("shaka-packager"); var file = GlobalUtil.FindExecutable("shaka-packager");
var file2 = GlobalUtil.FindExecutable("packager-linux-x64"); var file2 = GlobalUtil.FindExecutable("packager-linux-x64");
var file3 = GlobalUtil.FindExecutable("packager-osx-x64"); var file3 = GlobalUtil.FindExecutable("packager-osx-x64");
var file4 = GlobalUtil.FindExecutable("packager-win-x64"); var file4 = GlobalUtil.FindExecutable("packager-win-x64");
if (file == null && file2 == null && file3 == null && file4 == null) throw new FileNotFoundException("shaka-packager not found!"); if (file == null && file2 == null && file3 == null && file4 == null)
throw new FileNotFoundException(ResString.shakaPackagerNotFound);
option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4; option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}"); Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}");
break;
} }
else case DecryptEngine.MP4DECRYPT:
{ {
var file = GlobalUtil.FindExecutable("mp4decrypt"); var file = GlobalUtil.FindExecutable("mp4decrypt");
if (file == null) throw new FileNotFoundException("mp4decrypt not found!"); if (file == null) throw new FileNotFoundException(ResString.mp4decryptNotFound);
option.DecryptionBinaryPath = file; option.DecryptionBinaryPath = file;
Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}"); Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}");
break;
} }
} case DecryptEngine.FFMPEG:
else if (!File.Exists(option.DecryptionBinaryPath)) default:
{ option.DecryptionBinaryPath = option.FFmpegBinaryPath;
throw new FileNotFoundException(option.DecryptionBinaryPath); break;
} }
} }
//默认的headers // 默认的headers
var headers = new Dictionary<string, string>() var headers = new Dictionary<string, string>()
{ {
["user-agent"] = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" ["user-agent"] = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}; };
//添加或替换用户输入的headers // 添加或替换用户输入的headers
foreach (var item in option.Headers) foreach (var item in option.Headers)
{ {
headers[item.Key] = item.Value; headers[item.Key] = item.Value;
@ -185,14 +214,19 @@ namespace N_m3u8DL_RE
CustomeIV = option.CustomHLSIv, CustomeIV = option.CustomHLSIv,
}; };
//demo1 if (option.AllowHlsMultiExtMap)
{
parserConfig.CustomParserArgs.Add("AllowHlsMultiExtMap", "true");
}
// demo1
parserConfig.ContentProcessors.Insert(0, new DemoProcessor()); parserConfig.ContentProcessors.Insert(0, new DemoProcessor());
//demo2 // demo2
parserConfig.KeyProcessors.Insert(0, new DemoProcessor2()); parserConfig.KeyProcessors.Insert(0, new DemoProcessor2());
//for www.nowehoryzonty.pl // for www.nowehoryzonty.pl
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor()); parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
//等待任务开始时间 // 等待任务开始时间
if (option.TaskStartAt != null && option.TaskStartAt > DateTime.Now) if (option.TaskStartAt != null && option.TaskStartAt > DateTime.Now)
{ {
Logger.InfoMarkUp(ResString.taskStartAt + option.TaskStartAt); Logger.InfoMarkUp(ResString.taskStartAt + option.TaskStartAt);
@ -204,7 +238,7 @@ namespace N_m3u8DL_RE
var url = option.Input; var url = option.Input;
//流提取器配置 // 流提取器配置
var extractor = new StreamExtractor(parserConfig); var extractor = new StreamExtractor(parserConfig);
// 从链接加载内容 // 从链接加载内容
await RetryUtil.WebRequestRetryAsync(async () => await RetryUtil.WebRequestRetryAsync(async () =>
@ -212,33 +246,33 @@ namespace N_m3u8DL_RE
await extractor.LoadSourceFromUrlAsync(url); await extractor.LoadSourceFromUrlAsync(url);
return true; return true;
}); });
//解析流信息 // 解析流信息
var streams = await extractor.ExtractStreamsAsync(); var streams = await extractor.ExtractStreamsAsync();
//全部媒体 // 全部媒体
var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder); var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder).ToList();
//基本流 // 基本流
var basicStreams = lists.Where(x => x.MediaType == null || x.MediaType == MediaType.VIDEO); var basicStreams = lists.Where(x => x.MediaType is null or MediaType.VIDEO).ToList();
//可选音频轨道 // 可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO); var audios = lists.Where(x => x.MediaType == MediaType.AUDIO).ToList();
//可选字幕轨道 // 可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES); var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES).ToList();
//尝试从URL或文件读取文件名 // 尝试从URL或文件读取文件名
if (string.IsNullOrEmpty(option.SaveName)) if (string.IsNullOrEmpty(option.SaveName))
{ {
option.SaveName = OtherUtil.GetFileNameFromInput(option.Input); option.SaveName = OtherUtil.GetFileNameFromInput(option.Input);
} }
//生成文件夹 // 生成文件夹
var tmpDir = Path.Combine(option.TmpDir ?? Environment.CurrentDirectory, $"{option.SaveName ?? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}"); var tmpDir = Path.Combine(option.TmpDir ?? Environment.CurrentDirectory, $"{option.SaveName ?? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}");
//记录文件 // 记录文件
extractor.RawFiles["meta.json"] = GlobalUtil.ConvertToJson(lists); extractor.RawFiles["meta.json"] = GlobalUtil.ConvertToJson(lists);
//写出文件 // 写出文件
await WriteRawFilesAsync(option, extractor, tmpDir); await WriteRawFilesAsync(option, extractor, tmpDir);
Logger.Info(ResString.streamsInfo, lists.Count(), basicStreams.Count(), audios.Count(), subs.Count()); Logger.Info(ResString.streamsInfo, lists.Count, basicStreams.Count, audios.Count, subs.Count);
foreach (var item in lists) foreach (var item in lists)
{ {
@ -251,7 +285,7 @@ namespace N_m3u8DL_RE
basicStreams = FilterUtil.DoFilterDrop(basicStreams, option.DropVideoFilter); basicStreams = FilterUtil.DoFilterDrop(basicStreams, option.DropVideoFilter);
audios = FilterUtil.DoFilterDrop(audios, option.DropAudioFilter); audios = FilterUtil.DoFilterDrop(audios, option.DropAudioFilter);
subs = FilterUtil.DoFilterDrop(subs, option.DropSubtitleFilter); subs = FilterUtil.DoFilterDrop(subs, option.DropSubtitleFilter);
lists = basicStreams.Concat(audios).Concat(subs).OrderBy(x => true); lists = basicStreams.Concat(audios).Concat(subs).ToList();
} }
if (option.DropVideoFilter != null) Logger.Extra($"DropVideoFilter => {option.DropVideoFilter}"); if (option.DropVideoFilter != null) Logger.Extra($"DropVideoFilter => {option.DropVideoFilter}");
@ -263,7 +297,7 @@ namespace N_m3u8DL_RE
if (option.AutoSelect) if (option.AutoSelect)
{ {
if (basicStreams.Any()) if (basicStreams.Count != 0)
selectedStreams.Add(basicStreams.First()); selectedStreams.Add(basicStreams.First());
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language); var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
foreach (var lang in langs) foreach (var lang in langs)
@ -285,40 +319,40 @@ namespace N_m3u8DL_RE
} }
else else
{ {
//展示交互式选择框 // 展示交互式选择框
selectedStreams = FilterUtil.SelectStreams(lists); selectedStreams = FilterUtil.SelectStreams(lists);
} }
if (!selectedStreams.Any()) if (selectedStreams.Count == 0)
throw new Exception(ResString.noStreamsToDownload); throw new Exception(ResString.noStreamsToDownload);
//HLS: 选中流中若有没加载出playlist的加载playlist // HLS: 选中流中若有没加载出playlist的加载playlist
//DASH/MSS: 加载playlist (调用url预处理器) // DASH/MSS: 加载playlist (调用url预处理器)
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS) if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS)
await extractor.FetchPlayListAsync(selectedStreams); await extractor.FetchPlayListAsync(selectedStreams);
//直播检测 // 直播检测
var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod; var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod;
if (livingFlag) if (livingFlag)
{ {
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]"); Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]");
} }
//无法识别的加密方式,自动开启二进制合并 // 无法识别的加密方式,自动开启二进制合并
if (selectedStreams.Any(s => s.Playlist.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN)))) if (selectedStreams.Any(s => s.Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN))))
{ {
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]");
option.BinaryMerge = true; option.BinaryMerge = true;
} }
//应用用户自定义的分片范围 // 应用用户自定义的分片范围
if (!livingFlag) if (!livingFlag)
FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange); FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange);
//应用用户自定义的广告分片关键字 // 应用用户自定义的广告分片关键字
FilterUtil.CleanAd(selectedStreams, option.AdKeywords); FilterUtil.CleanAd(selectedStreams, option.AdKeywords);
//记录文件 // 记录文件
extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams); extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams);
Logger.Info(ResString.selectedStream); Logger.Info(ResString.selectedStream);
@ -327,7 +361,7 @@ namespace N_m3u8DL_RE
Logger.InfoMarkUp(item.ToString()); Logger.InfoMarkUp(item.ToString());
} }
//写出文件 // 写出文件
await WriteRawFilesAsync(option, extractor, tmpDir); await WriteRawFilesAsync(option, extractor, tmpDir);
if (option.SkipDownload) if (option.SkipDownload)
@ -342,19 +376,19 @@ namespace N_m3u8DL_RE
Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]"); Logger.InfoMarkUp(ResString.saveName + $"[deepskyblue1]{option.SaveName.EscapeMarkup()}[/]");
//开始MuxAfterDone后自动使用二进制版 // 开始MuxAfterDone后自动使用二进制版
if (!option.BinaryMerge && option.MuxAfterDone) if (option is { BinaryMerge: false, MuxAfterDone: true })
{ {
option.BinaryMerge = true; option.BinaryMerge = true;
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge6}[/]");
} }
//下载配置 // 下载配置
var downloadConfig = new DownloaderConfig() var downloadConfig = new DownloaderConfig()
{ {
MyOptions = option, MyOptions = option,
DirPrefix = tmpDir, DirPrefix = tmpDir,
Headers = parserConfig.Headers, //使用命令行解析得到的Headers Headers = parserConfig.Headers, // 使用命令行解析得到的Headers
}; };
var result = false; var result = false;
@ -366,7 +400,7 @@ namespace N_m3u8DL_RE
} }
else if (!livingFlag) else if (!livingFlag)
{ {
//开始下载 // 开始下载
var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor); var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor);
result = await sdm.StartDownloadAsync(); result = await sdm.StartDownloadAsync();
} }
@ -389,7 +423,7 @@ namespace N_m3u8DL_RE
private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir) private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir)
{ {
//写出json文件 // 写出json文件
if (option.WriteMetaJson) if (option.WriteMetaJson)
{ {
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir); if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
@ -422,31 +456,27 @@ namespace N_m3u8DL_RE
} }
} }
//重定向 // 重定向
static async Task<string> Get302Async(string url) static async Task<string> Get302Async(string url)
{ {
//this allows you to set the settings so that we can get the redirect url // this allows you to set the settings so that we can get the redirect url
var handler = new HttpClientHandler() var handler = new HttpClientHandler
{ {
AllowAutoRedirect = false AllowAutoRedirect = false
}; };
string redirectedUrl = ""; var redirectedUrl = "";
using (HttpClient client = new(handler)) using var client = new HttpClient(handler);
using (HttpResponseMessage response = await client.GetAsync(url)) using var response = await client.GetAsync(url);
using (HttpContent content = response.Content) using var content = response.Content;
{
// ... Read the response to see if we have the redirected url // ... Read the response to see if we have the redirected url
if (response.StatusCode == System.Net.HttpStatusCode.Found) if (response.StatusCode != HttpStatusCode.Found) return redirectedUrl;
{
HttpResponseHeaders headers = response.Headers; var headers = response.Headers;
if (headers != null && headers.Location != null) if (headers.Location != null)
{ {
redirectedUrl = headers.Location.AbsoluteUri; redirectedUrl = headers.Location.AbsoluteUri;
} }
}
}
return redirectedUrl; return redirectedUrl;
} }
}
} }

View File

@ -2,14 +2,12 @@
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using System.IO;
using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static class DownloadUtil
{ {
internal class DownloadUtil
{
private static readonly HttpClient AppHttpClient = HTTPUtil.AppHttpClient; private static readonly HttpClient AppHttpClient = HTTPUtil.AppHttpClient;
private static async Task<DownloadResult> CopyFileAsync(string sourceFile, string path, SpeedContainer speedContainer, long? fromPosition = null, long? toPosition = null) private static async Task<DownloadResult> CopyFileAsync(string sourceFile, string path, SpeedContainer speedContainer, long? fromPosition = null, long? toPosition = null)
@ -26,8 +24,8 @@ namespace N_m3u8DL_RE.Util
else else
{ {
var buffer = new byte[expect]; var buffer = new byte[expect];
await inputStream.ReadAsync(buffer); _ = await inputStream.ReadAsync(buffer);
await outputStream.WriteAsync(buffer, 0, buffer.Length); await outputStream.WriteAsync(buffer);
speedContainer.Add(buffer.Length); speedContainer.Add(buffer.Length);
} }
return new DownloadResult() return new DownloadResult()
@ -83,7 +81,7 @@ namespace N_m3u8DL_RE.Util
{ {
HttpResponseHeaders respHeaders = response.Headers; HttpResponseHeaders respHeaders = response.Headers;
Logger.Debug(respHeaders.ToString()); Logger.Debug(respHeaders.ToString());
if (respHeaders != null && respHeaders.Location != null) if (respHeaders.Location != null)
{ {
var redirectedUrl = ""; var redirectedUrl = "";
if (!respHeaders.Location.IsAbsoluteUri) if (!respHeaders.Location.IsAbsoluteUri)
@ -110,17 +108,17 @@ namespace N_m3u8DL_RE.Util
size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token); size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);
speedContainer.Add(size); speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size); await stream.WriteAsync(buffer.AsMemory(0, size));
//检测imageHeader // 检测imageHeader
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer); bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
//检测GZipFor DDP Audio // 检测GZipFor DDP Audio
bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b; bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0) while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
{ {
speedContainer.Add(size); speedContainer.Add(size);
await stream.WriteAsync(buffer, 0, size); await stream.WriteAsync(buffer.AsMemory(0, size));
//限速策略 // 限速策略
while (speedContainer.Downloaded > speedContainer.SpeedLimit) while (speedContainer.Downloaded > speedContainer.SpeedLimit)
{ {
await Task.Delay(1); await Task.Delay(1);
@ -142,5 +140,4 @@ namespace N_m3u8DL_RE.Util
throw new Exception("Download speed too slow!"); throw new Exception("Download speed too slow!");
} }
} }
}
} }

View File

@ -4,20 +4,15 @@ using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
public static class FilterUtil
{ {
public class FilterUtil
{
public static List<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> lists, StreamFilter? filter) public static List<StreamSpec> DoFilterKeep(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{ {
if (filter == null) return new List<StreamSpec>(); if (filter == null) return [];
var inputs = lists.Where(_ => true); var inputs = lists.Where(_ => true);
if (filter.GroupIdReg != null) if (filter.GroupIdReg != null)
@ -56,13 +51,13 @@ namespace N_m3u8DL_RE.Util
var bestNumberStr = filter.For.Replace("best", ""); var bestNumberStr = filter.For.Replace("best", "");
var worstNumberStr = filter.For.Replace("worst", ""); var worstNumberStr = filter.For.Replace("worst", "");
if (filter.For == "best" && inputs.Count() > 0) if (filter.For == "best" && inputs.Any())
inputs = inputs.Take(1).ToList(); inputs = inputs.Take(1).ToList();
else if (filter.For == "worst" && inputs.Count() > 0) else if (filter.For == "worst" && inputs.Any())
inputs = inputs.TakeLast(1).ToList(); inputs = inputs.TakeLast(1).ToList();
else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0) else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Any())
inputs = inputs.Take(bestNumber).ToList(); inputs = inputs.Take(bestNumber).ToList();
else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0) else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Any())
inputs = inputs.TakeLast(worstNumber).ToList(); inputs = inputs.TakeLast(worstNumber).ToList();
return inputs.ToList(); return inputs.ToList();
@ -70,7 +65,7 @@ namespace N_m3u8DL_RE.Util
public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter) public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)
{ {
if (filter == null) return new List<StreamSpec>(lists); if (filter == null) return [..lists];
var inputs = lists.Where(_ => true); var inputs = lists.Where(_ => true);
var selected = DoFilterKeep(lists, filter); var selected = DoFilterKeep(lists, filter);
@ -82,23 +77,23 @@ namespace N_m3u8DL_RE.Util
public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists) public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
{ {
if (lists.Count() == 1) var streamSpecs = lists.ToList();
return new List<StreamSpec>(lists); if (streamSpecs.Count == 1)
return [..streamSpecs];
//基本流 // 基本流
var basicStreams = lists.Where(x => x.MediaType == null); var basicStreams = streamSpecs.Where(x => x.MediaType == null).ToList();
//可选音频轨道 // 可选音频轨道
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO); var audios = streamSpecs.Where(x => x.MediaType == MediaType.AUDIO).ToList();
//可选字幕轨道 // 可选字幕轨道
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES); var subs = streamSpecs.Where(x => x.MediaType == MediaType.SUBTITLES).ToList();
var prompt = new MultiSelectionPrompt<StreamSpec>() var prompt = new MultiSelectionPrompt<StreamSpec>()
.Title(ResString.promptTitle) .Title(ResString.promptTitle)
.UseConverter(x => .UseConverter(x =>
{ {
if (x.Name != null && x.Name.StartsWith("__")) if (x.Name != null && x.Name.StartsWith("__"))
return $"[darkslategray1]{x.Name.Substring(2)}[/]"; return $"[darkslategray1]{x.Name[2..]}[/]";
else
return x.ToString().EscapeMarkup().RemoveMarkup(); return x.ToString().EscapeMarkup().RemoveMarkup();
}) })
.Required() .Required()
@ -107,38 +102,38 @@ namespace N_m3u8DL_RE.Util
.InstructionsText(ResString.promptInfo) .InstructionsText(ResString.promptInfo)
; ;
//默认选中第一个 // 默认选中第一个
var first = lists.First(); var first = streamSpecs.First();
prompt.Select(first); prompt.Select(first);
if (basicStreams.Any()) if (basicStreams.Count != 0)
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams);
} }
if (audios.Any()) if (audios.Count != 0)
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios);
//默认音轨 // 默认音轨
if (first.AudioId != null) if (first.AudioId != null)
{ {
prompt.Select(audios.First(a => a.GroupId == first.AudioId)); prompt.Select(audios.First(a => a.GroupId == first.AudioId));
} }
} }
if (subs.Any()) if (subs.Count != 0)
{ {
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs); prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs);
//默认字幕轨 // 默认字幕轨
if (first.SubtitleId != null) if (first.SubtitleId != null)
{ {
prompt.Select(subs.First(s => s.GroupId == first.SubtitleId)); prompt.Select(subs.First(s => s.GroupId == first.SubtitleId));
} }
} }
//如果此时还是没有选中任何流,自动选择一个 // 如果此时还是没有选中任何流,自动选择一个
prompt.Select(basicStreams.Concat(audios).Concat(subs).First()); prompt.Select(basicStreams.Concat(audios).Concat(subs).First());
//多选 // 多选
var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt); var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt);
return selectedStreams; return selectedStreams;
@ -147,11 +142,11 @@ namespace N_m3u8DL_RE.Util
/// <summary> /// <summary>
/// 直播使用。对齐各个轨道的起始。 /// 直播使用。对齐各个轨道的起始。
/// </summary> /// </summary>
/// <param name="streams"></param> /// <param name="selectedSteams"></param>
/// <param name="takeLastCount"></param> /// <param name="takeLastCount"></param>
public static void SyncStreams(List<StreamSpec> selectedSteams, int takeLastCount = 15) public static void SyncStreams(List<StreamSpec> selectedSteams, int takeLastCount = 15)
{ {
//通过Date同步 // 通过Date同步
if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null))) if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null)))
{ {
var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!; var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!;
@ -159,12 +154,12 @@ namespace N_m3u8DL_RE.Util
{ {
foreach (var part in item.Playlist!.MediaParts) foreach (var part in item.Playlist!.MediaParts)
{ {
//秒级同步 忽略毫秒 // 秒级同步 忽略毫秒
part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList(); part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList();
} }
} }
} }
else //通过index同步 else // 通过index同步
{ {
var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index)); var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index));
foreach (var item in selectedSteams) foreach (var item in selectedSteams)
@ -176,7 +171,7 @@ namespace N_m3u8DL_RE.Util
} }
} }
//取最新的N个分片 // 取最新的N个分片
if (selectedSteams.Any(x => x.Playlist!.MediaParts[0].MediaSegments.Count > takeLastCount)) if (selectedSteams.Any(x => x.Playlist!.MediaParts[0].MediaSegments.Count > takeLastCount))
{ {
var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1; var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1;
@ -198,17 +193,15 @@ namespace N_m3u8DL_RE.Util
/// <param name="customRange"></param> /// <param name="customRange"></param>
public static void ApplyCustomRange(List<StreamSpec> selectedSteams, CustomRange? customRange) public static void ApplyCustomRange(List<StreamSpec> selectedSteams, CustomRange? customRange)
{ {
var resultList = selectedSteams.Select(x => 0d).ToList();
if (customRange == null) return; if (customRange == null) return;
Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]"); Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]");
Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]"); Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]");
var filteByIndex = customRange.StartSegIndex != null && customRange.EndSegIndex != null; var filterByIndex = customRange is { StartSegIndex: not null, EndSegIndex: not null };
var filteByTime = customRange.StartSec != null && customRange.EndSec != null; var filterByTime = customRange is { StartSec: not null, EndSec: not null };
if (!filteByIndex && !filteByTime) if (!filterByIndex && !filterByTime)
{ {
Logger.ErrorMarkUp(ResString.customRangeInvalid); Logger.ErrorMarkUp(ResString.customRangeInvalid);
return; return;
@ -220,8 +213,8 @@ namespace N_m3u8DL_RE.Util
if (stream.Playlist == null) continue; if (stream.Playlist == null) continue;
foreach (var part in stream.Playlist.MediaParts) foreach (var part in stream.Playlist.MediaParts)
{ {
var newSegments = new List<MediaSegment>(); List<MediaSegment> newSegments;
if (filteByIndex) if (filterByIndex)
newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList(); newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList();
else else
newSegments = part.MediaSegments.Where(seg => stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) >= customRange.StartSec newSegments = part.MediaSegments.Where(seg => stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) >= customRange.StartSec
@ -239,11 +232,11 @@ namespace N_m3u8DL_RE.Util
/// 根据用户输入,清除广告分片 /// 根据用户输入,清除广告分片
/// </summary> /// </summary>
/// <param name="selectedSteams"></param> /// <param name="selectedSteams"></param>
/// <param name="customRange"></param> /// <param name="keywords"></param>
public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords) public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords)
{ {
if (keywords == null) return; if (keywords == null) return;
var regList = keywords.Select(s => new Regex(s)); var regList = keywords.Select(s => new Regex(s)).ToList();
foreach ( var reg in regList) foreach ( var reg in regList)
{ {
Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]"); Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]");
@ -257,19 +250,16 @@ namespace N_m3u8DL_RE.Util
foreach (var part in stream.Playlist.MediaParts) foreach (var part in stream.Playlist.MediaParts)
{ {
//没有找到广告分片 // 没有找到广告分片
if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url)))) if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url))))
{ {
continue; continue;
} }
//找到广告分片 清理 // 找到广告分片 清理
else
{
part.MediaSegments = part.MediaSegments.Where(x => regList.All(reg => !reg.IsMatch(x.Url))).ToList(); part.MediaSegments = part.MediaSegments.Where(x => regList.All(reg => !reg.IsMatch(x.Url))).ToList();
} }
}
//清理已经为空的 part // 清理已经为空的 part
stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList(); stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList();
var countAfter = stream.SegmentsCount; var countAfter = stream.SegmentsCount;
@ -280,5 +270,4 @@ namespace N_m3u8DL_RE.Util
} }
} }
} }
}
} }

View File

@ -1,28 +1,21 @@
using System; namespace N_m3u8DL_RE.Util;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util internal static class ImageHeaderUtil
{ {
internal class ImageHeaderUtil
{
public static bool IsImageHeader(byte[] bArr) public static bool IsImageHeader(byte[] bArr)
{ {
var size = bArr.Length; var size = bArr.Length;
//PNG HEADER检测 // PNG HEADER检测
if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3]) if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3])
return true; return true;
//GIF HEADER检测 // GIF HEADER检测
else if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3]) if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3])
return true; return true;
//BMP HEADER检测 // BMP HEADER检测
else if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8]) if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8])
return true; return true;
//JPEG HEADER检测 // JPEG HEADER检测
else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2]) if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2])
return true; return true;
return false; return false;
} }
@ -31,7 +24,7 @@ namespace N_m3u8DL_RE.Util
{ {
var sourceData = await File.ReadAllBytesAsync(sourcePath); var sourceData = await File.ReadAllBytesAsync(sourcePath);
//PNG HEADER // PNG HEADER
if (137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3]) if (137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3])
{ {
if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119]) if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119])
@ -44,7 +37,7 @@ namespace N_m3u8DL_RE.Util
sourceData = sourceData[771..]; sourceData = sourceData[771..];
else else
{ {
//手动查询结尾标记 0x47 出现两次 // 手动查询结尾标记 0x47 出现两次
int skip = 0; int skip = 0;
for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)
{ {
@ -57,20 +50,20 @@ namespace N_m3u8DL_RE.Util
sourceData = sourceData[skip..]; sourceData = sourceData[skip..];
} }
} }
//GIF HEADER // GIF HEADER
else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3]) else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3])
{ {
sourceData = sourceData[42..]; sourceData = sourceData[42..];
} }
//BMP HEADER // BMP HEADER
else if (0x42 == sourceData[0] && 0x4D == sourceData[1] && 0x00 == sourceData[5] && 0x00 == sourceData[6] && 0x00 == sourceData[7] && 0x00 == sourceData[8]) else if (0x42 == sourceData[0] && 0x4D == sourceData[1] && 0x00 == sourceData[5] && 0x00 == sourceData[6] && 0x00 == sourceData[7] && 0x00 == sourceData[8])
{ {
sourceData = sourceData[0x3E..]; sourceData = sourceData[0x3E..];
} }
//JPEG HEADER检测 // JPEG HEADER检测
else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2]) else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2])
{ {
//手动查询结尾标记 0x47 出现两次 // 手动查询结尾标记 0x47 出现两次
int skip = 0; int skip = 0;
for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++) for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)
{ {
@ -85,5 +78,4 @@ namespace N_m3u8DL_RE.Util
await File.WriteAllBytesAsync(sourcePath, sourceData); await File.WriteAllBytesAsync(sourcePath, sourceData);
} }
}
} }

View File

@ -1,34 +1,20 @@
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal class Language(string extendCode, string code, string desc, string descA)
{ {
class Language public readonly string Code = code;
{ public readonly string ExtendCode = extendCode;
public string Code; public readonly string Description = desc;
public string ExtendCode; public readonly string DescriptionAudio = descA;
public string Description; }
public string DescriptionAudio;
public Language(string extendCode, string code, string desc, string descA) internal static class LanguageCodeUtil
{ {
Code = code;
ExtendCode = extendCode;
Description = desc;
DescriptionAudio = descA;
}
}
internal class LanguageCodeUtil private static readonly List<Language> ALL_LANGS = @"
{ default;und;default;default
private LanguageCodeUtil() { }
private readonly static List<Language> ALL_LANGS = @"
af;afr;Afrikaans;Afrikaans af;afr;Afrikaans;Afrikaans
af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa) af-ZA;afr;Afrikaans (South Africa);Afrikaans (South Africa)
am;amh;Amharic;Amharic am;amh;Amharic;Amharic
@ -388,11 +374,11 @@ CC;chi;中文(繁體);中文
CZ;chi;; CZ;chi;;
MA;msa;Melayu;Melayu MA;msa;Melayu;Melayu
" "
.Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => .Trim().Replace("\r", "").Split('\n').Where(x => !string.IsNullOrWhiteSpace(x)).Select(x =>
{ {
var arr = x.Trim().Split(';'); var arr = x.Trim().Split(';', StringSplitOptions.TrimEntries);
return new Language(arr[0].Trim(), arr[1].Trim(), arr[2].Trim(), arr[3].Trim()); return new Language(arr[0], arr[1], arr[2], arr[3]);
}).ToList(); }).ToList();
private static Dictionary<string, string> CODE_MAP = @" private static Dictionary<string, string> CODE_MAP = @"
iv;IVL iv;IVL
@ -500,13 +486,12 @@ nn;nno
bs;bos bs;bos
sr;srp sr;srp
" "
.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()); .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());
private static string ConvertTwoToThree(string input) private static string ConvertTwoToThree(string input)
{ {
if (CODE_MAP.TryGetValue(input, out var code)) return code; return CODE_MAP.GetValueOrDefault(input, input);
return input;
} }
/// <summary> /// <summary>
@ -519,12 +504,12 @@ sr;srp
if (string.IsNullOrEmpty(outputFile.LangCode)) return; if (string.IsNullOrEmpty(outputFile.LangCode)) return;
var originalLangCode = outputFile.LangCode; var originalLangCode = outputFile.LangCode;
//先直接查找 // 先直接查找
var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase)); var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase));
//处理特殊的扩展语言标记 // 处理特殊的扩展语言标记
if (lang == null) if (lang == null)
{ {
//2位转3位 // 2位转3位
var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First()); var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First());
lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase)); lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase));
} }
@ -535,13 +520,12 @@ sr;srp
if (string.IsNullOrEmpty(outputFile.Description)) if (string.IsNullOrEmpty(outputFile.Description))
outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio; outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio;
} }
else if (outputFile.LangCode == null) else
{ {
outputFile.LangCode = "und"; //无法识别直接置为und outputFile.LangCode = "und"; // 无法识别直接置为und
} }
//无描述则把LangCode当作描述 // 无描述则把LangCode当作描述
if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode; if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode;
} }
}
} }

View File

@ -1,31 +1,23 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Common.Util;
using NiL.JS.Expressions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static class LargeSingleFileSplitUtil
{ {
internal class LargeSingleFileSplitUtil
{
class Clip class Clip
{ {
public required int index; public required int Index;
public required long from; public required long From;
public required long to; public required long To;
} }
/// <summary> /// <summary>
/// URL大文件切片处理 /// URL大文件切片处理
/// </summary> /// </summary>
/// <param name="url"></param> /// <param name="segment"></param>
/// <param name="headers"></param> /// <param name="headers"></param>
/// <param name="splitSegments"></param>
/// <returns></returns> /// <returns></returns>
public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers) public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers)
{ {
@ -43,10 +35,10 @@ namespace N_m3u8DL_RE.Util
{ {
splitSegments.Add(new MediaSegment() splitSegments.Add(new MediaSegment()
{ {
Index = clip.index, Index = clip.Index,
Url = url, Url = url,
StartRange = clip.from, StartRange = clip.From,
ExpectLength = clip.to == -1 ? null : clip.to - clip.from + 1, ExpectLength = clip.To == -1 ? null : clip.To - clip.From + 1,
EncryptInfo = segment.EncryptInfo, EncryptInfo = segment.EncryptInfo,
}); });
} }
@ -85,10 +77,10 @@ namespace N_m3u8DL_RE.Util
return totalSizeBytes; return totalSizeBytes;
} }
//此函数主要是切片下载逻辑 // 此函数主要是切片下载逻辑
private static List<Clip> GetAllClips(string url, long fileSize) private static List<Clip> GetAllClips(string url, long fileSize)
{ {
List<Clip> clips = new(); List<Clip> clips = [];
int index = 0; int index = 0;
long counter = 0; long counter = 0;
int perSize = 10 * 1024 * 1024; int perSize = 10 * 1024 * 1024;
@ -96,11 +88,11 @@ namespace N_m3u8DL_RE.Util
{ {
Clip c = new() Clip c = new()
{ {
index = index, Index = index,
from = counter, From = counter,
to = counter + perSize To = counter + perSize
}; };
//没到最后 // 没到最后
if (fileSize - perSize > 0) if (fileSize - perSize > 0)
{ {
fileSize -= perSize; fileSize -= perSize;
@ -108,15 +100,14 @@ namespace N_m3u8DL_RE.Util
index++; index++;
clips.Add(c); clips.Add(c);
} }
//已到最后 // 已到最后
else else
{ {
c.to = -1; c.To = -1;
clips.Add(c); clips.Add(c);
break; break;
} }
} }
return clips; return clips;
} }
}
} }

View File

@ -1,21 +1,25 @@
using Mp4SubtitleParser; using Mp4SubtitleParser;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using N_m3u8DL_RE.Config;
using System.Diagnostics; using System.Diagnostics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using N_m3u8DL_RE.Enum;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static partial class MP4DecryptUtil
{ {
internal class MP4DecryptUtil private static readonly string ZeroKid = "00000000000000000000000000000000";
{ public static async Task<bool> DecryptAsync(DecryptEngine decryptEngine, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false)
private static string ZeroKid = "00000000000000000000000000000000";
public static async Task<bool> DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false)
{ {
if (keys == null || keys.Length == 0) return false; if (keys == null || keys.Length == 0) return false;
var keyPairs = keys.ToList();
string? keyPair = null; string? keyPair = null;
string? trackId = null; string? trackId = null;
string? tmpEncFile = null;
string? tmpDecFile = null;
string? workDir = null;
if (isMultiDRM) if (isMultiDRM)
{ {
@ -24,80 +28,117 @@ namespace N_m3u8DL_RE.Util
if (!string.IsNullOrEmpty(kid)) if (!string.IsNullOrEmpty(kid))
{ {
var test = keys.Where(k => k.StartsWith(kid)); var test = keyPairs.Where(k => k.StartsWith(kid)).ToList();
if (test.Any()) keyPair = test.First(); if (test.Count != 0) keyPair = test.First();
} }
//Apple // Apple
if (kid == ZeroKid) if (kid == ZeroKid)
{ {
keyPair = keys.First(); keyPair = keyPairs.First();
trackId = "1"; trackId = "1";
} }
// user only input key, append kid
if (keyPair == null && keyPairs.Count == 1 && !keyPairs.First().Contains(':'))
{
keyPairs = keyPairs.Select(x => $"{kid}:{x}").ToList();
keyPair = keyPairs.First();
}
if (keyPair == null) return false; if (keyPair == null) return false;
//shakaPackager 无法单独解密init文件 // shakaPackager/ffmpeg 无法单独解密init文件
if (source.EndsWith("_init.mp4") && shakaPackager) return false; if (source.EndsWith("_init.mp4") && decryptEngine != DecryptEngine.MP4DECRYPT) return false;
var cmd = ""; string cmd;
var tmpFile = ""; var tmpFile = "";
if (shakaPackager) if (decryptEngine == DecryptEngine.SHAKA_PACKAGER)
{ {
var enc = source; var enc = source;
//shakaPackager 手动构造文件 // shakaPackager 手动构造文件
if (init != "") if (init != "")
{ {
tmpFile = Path.ChangeExtension(source, ".itmp"); tmpFile = Path.ChangeExtension(source, ".itmp");
MergeUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile); MergeUtil.CombineMultipleFilesIntoSingleFile([init, source], tmpFile);
enc = tmpFile; enc = tmpFile;
} }
cmd = $"--quiet --enable_raw_key_decryption input=\"{enc}\",stream=0,output=\"{dest}\" " + cmd = $"--quiet --enable_raw_key_decryption input=\"{enc}\",stream=0,output=\"{dest}\" " +
$"--keys {(trackId != null ? $"label={trackId}:" : "")}key_id={(trackId != null ? ZeroKid : kid)}:key={keyPair.Split(':')[1]}"; $"--keys {(trackId != null ? $"label={trackId}:" : "")}key_id={(trackId != null ? ZeroKid : kid)}:key={keyPair.Split(':')[1]}";
} }
else else if (decryptEngine == DecryptEngine.MP4DECRYPT)
{ {
if (trackId == null) if (trackId == null)
{ {
cmd = string.Join(" ", keys.Select(k => $"--key {k}")); cmd = string.Join(" ", keyPairs.Select(k => $"--key {k}"));
} }
else else
{ {
cmd = string.Join(" ", keys.Select(k => $"--key {trackId}:{k.Split(':')[1]}")); cmd = string.Join(" ", keyPairs.Select(k => $"--key {trackId}:{k.Split(':')[1]}"));
} }
// 解决mp4decrypt中文问题 切换到源文件所在目录并改名再解密
workDir = Path.GetDirectoryName(source)!;
tmpEncFile = Path.Combine(workDir, $"{Guid.NewGuid()}{Path.GetExtension(source)}");
tmpDecFile = Path.Combine(workDir, $"{Path.GetFileNameWithoutExtension(tmpEncFile)}_dec{Path.GetExtension(tmpEncFile)}");
File.Move(source, tmpEncFile);
if (init != "") if (init != "")
{ {
cmd += $" --fragments-info \"{init}\" "; var infoFile = Path.GetDirectoryName(init) == workDir ? Path.GetFileName(init) : init;
cmd += $" --fragments-info \"{infoFile}\" ";
} }
cmd += $" \"{source}\" \"{dest}\""; cmd += $" \"{Path.GetFileName(tmpEncFile)}\" \"{Path.GetFileName(tmpDecFile)}\"";
}
else
{
var enc = source;
// ffmpeg实时解密 手动构造文件
if (init != "")
{
tmpFile = Path.ChangeExtension(source, ".itmp");
MergeUtil.CombineMultipleFilesIntoSingleFile([init, source], tmpFile);
enc = tmpFile;
} }
await RunCommandAsync(bin, cmd); cmd = $"-loglevel error -nostdin -decryption_key {keyPair.Split(':')[1]} -i \"{enc}\" -c copy \"{dest}\"";
}
if (File.Exists(dest) && new FileInfo(dest).Length > 0) var isSuccess = await RunCommandAsync(bin, cmd, workDir);
// mp4decrypt 还原文件改名操作
if (workDir is not null)
{
if (File.Exists(tmpEncFile)) File.Move(tmpEncFile, source);
if (File.Exists(tmpDecFile)) File.Move(tmpDecFile, dest);
}
if (isSuccess)
{ {
if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile); if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile);
return true; return true;
} }
Logger.Error(ResString.decryptionFailed);
return false; return false;
} }
private static async Task RunCommandAsync(string name, string arg) private static async Task<bool> RunCommandAsync(string name, string arg, string? workDir = null)
{ {
Logger.DebugMarkUp($"FileName: {name}"); Logger.DebugMarkUp($"FileName: {name}");
Logger.DebugMarkUp($"Arguments: {arg}"); Logger.DebugMarkUp($"Arguments: {arg}");
await Process.Start(new ProcessStartInfo() var process = Process.Start(new ProcessStartInfo()
{ {
FileName = name, FileName = name,
Arguments = arg, Arguments = arg,
//RedirectStandardOutput = true, // RedirectStandardOutput = true,
//RedirectStandardError = true, // RedirectStandardError = true,
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false UseShellExecute = false,
})!.WaitForExitAsync(); WorkingDirectory = workDir
});
await process!.WaitForExitAsync();
return process.ExitCode == 0;
} }
/// <summary> /// <summary>
@ -116,16 +157,14 @@ namespace N_m3u8DL_RE.Util
Logger.InfoMarkUp(ResString.searchKey); Logger.InfoMarkUp(ResString.searchKey);
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
var line = ""; while (await reader.ReadLineAsync() is { } line)
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.Trim().StartsWith(kid))
{ {
if (!line.Trim().StartsWith(kid)) continue;
Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]"); Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]");
return line.Trim(); return line.Trim();
} }
} }
}
catch (Exception ex) catch (Exception ex)
{ {
Logger.ErrorMarkUp(ex.Message); Logger.ErrorMarkUp(ex.Message);
@ -144,17 +183,15 @@ namespace N_m3u8DL_RE.Util
public static ParsedMP4Info GetMP4Info(string output) public static ParsedMP4Info GetMP4Info(string output)
{ {
using (var fs = File.OpenRead(output)) using var fs = File.OpenRead(output);
{ var header = new byte[1 * 1024 * 1024]; // 1MB
var header = new byte[1 * 1024 * 1024]; //1MB _ = fs.Read(header);
fs.Read(header);
return GetMP4Info(header); return GetMP4Info(header);
} }
}
public static string? ReadInitShaka(string output, string bin) public static string? ReadInitShaka(string output, string bin)
{ {
Regex ShakaKeyIDRegex = new Regex("Key for key_id=([0-9a-f]+) was not found"); Regex shakaKeyIdRegex = KidOutputRegex();
// TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid) // TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid)
// - stop process // - stop process
@ -174,7 +211,9 @@ namespace N_m3u8DL_RE.Util
p.Start(); p.Start();
var errorOutput = p.StandardError.ReadToEnd(); var errorOutput = p.StandardError.ReadToEnd();
p.WaitForExit(); p.WaitForExit();
return ShakaKeyIDRegex.Match(errorOutput).Groups[1].Value; return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value;
}
} }
[GeneratedRegex("Key for key_id=([0-9a-f]+) was not found")]
private static partial Regex KidOutputRegex();
} }

View File

@ -1,36 +1,30 @@
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static partial class MediainfoUtil
{ {
internal partial class MediainfoUtil
{
[GeneratedRegex(" Stream #.*")] [GeneratedRegex(" Stream #.*")]
private static partial Regex TextRegex(); private static partial Regex TextRegex();
[GeneratedRegex("#0:\\d(\\[0x\\w+?\\])")] [GeneratedRegex(@"#0:\d(\[0x\w+?\])")]
private static partial Regex IdRegex(); private static partial Regex IdRegex();
[GeneratedRegex(": (\\w+): (.*)")] [GeneratedRegex(": (\\w+): (.*)")]
private static partial Regex TypeRegex(); private static partial Regex TypeRegex();
[GeneratedRegex("(.*?)(,|$)")] [GeneratedRegex("(.*?)(,|$)")]
private static partial Regex BaseInfoRegex(); private static partial Regex BaseInfoRegex();
[GeneratedRegex(" \\/ 0x\\w+")] [GeneratedRegex(@" \/ 0x\w+")]
private static partial Regex ReplaceRegex(); private static partial Regex ReplaceRegex();
[GeneratedRegex("\\d{2,}x\\d+")] [GeneratedRegex(@"\d{2,}x\d+")]
private static partial Regex ResRegex(); private static partial Regex ResRegex();
[GeneratedRegex("\\d+ kb\\/s")] [GeneratedRegex(@"\d+ kb\/s")]
private static partial Regex BitrateRegex(); private static partial Regex BitrateRegex();
[GeneratedRegex("(\\d+(\\.\\d+)?) fps")] [GeneratedRegex(@"(\d+(\.\d+)?) fps")]
private static partial Regex FpsRegex(); private static partial Regex FpsRegex();
[GeneratedRegex("DOVI configuration record.*profile: (\\d).*compatibility id: (\\d)")] [GeneratedRegex(@"DOVI configuration record.*profile: (\d).*compatibility id: (\d)")]
private static partial Regex DoViRegex(); private static partial Regex DoViRegex();
[GeneratedRegex("Duration.*?start: (\\d+\\.?\\d{0,3})")] [GeneratedRegex(@"Duration.*?start: (\d+\.?\d{0,3})")]
private static partial Regex StartRegex(); private static partial Regex StartRegex();
public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file) public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file)
@ -48,7 +42,7 @@ namespace N_m3u8DL_RE.Util
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false UseShellExecute = false
})!; })!;
var output = p.StandardError.ReadToEnd(); var output = await p.StandardError.ReadToEndAsync();
await p.WaitForExitAsync(); await p.WaitForExitAsync();
foreach (Match stream in TextRegex().Matches(output)) foreach (Match stream in TextRegex().Matches(output))
@ -87,7 +81,7 @@ namespace N_m3u8DL_RE.Util
if (result.Count == 0) if (result.Count == 0)
{ {
result.Add(new Mediainfo() result.Add(new Mediainfo
{ {
Type = "Unknown" Type = "Unknown"
}); });
@ -95,5 +89,4 @@ namespace N_m3u8DL_RE.Util
return result; return result;
} }
}
} }

View File

@ -1,21 +1,14 @@
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Entity; using N_m3u8DL_RE.Entity;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using N_m3u8DL_RE.Enum;
using System.Xml;
using System.Xml.Linq;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static class MergeUtil
{ {
internal class MergeUtil
{
/// <summary> /// <summary>
/// 输入一堆已存在的文件,合并到新文件 /// 输入一堆已存在的文件,合并到新文件
/// </summary> /// </summary>
@ -34,20 +27,16 @@ namespace N_m3u8DL_RE.Util
if (!Directory.Exists(Path.GetDirectoryName(outputFilePath))) if (!Directory.Exists(Path.GetDirectoryName(outputFilePath)))
Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!);
string[] inputFilePaths = files; var inputFilePaths = files;
using (var outputStream = File.Create(outputFilePath)) using var outputStream = File.Create(outputFilePath);
{
foreach (var inputFilePath in inputFilePaths) foreach (var inputFilePath in inputFilePaths)
{ {
if (inputFilePath == "") if (inputFilePath == "")
continue; continue;
using (var inputStream = File.OpenRead(inputFilePath)) using var inputStream = File.OpenRead(inputFilePath);
{
inputStream.CopyTo(outputStream); inputStream.CopyTo(outputStream);
} }
} }
}
}
private static int InvokeFFmpeg(string binary, string command, string workingDirectory) private static int InvokeFFmpeg(string binary, string command, string workingDirectory)
{ {
@ -79,25 +68,21 @@ namespace N_m3u8DL_RE.Util
public static string[] PartialCombineMultipleFiles(string[] files) public static string[] PartialCombineMultipleFiles(string[] files)
{ {
var newFiles = new List<string>(); var newFiles = new List<string>();
int div = 0; var div = files.Length <= 90000 ? 100 : 200;
if (files.Length <= 90000)
div = 100;
else
div = 200;
string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T"); var outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T");
int index = 0; //序号 var index = 0; // 序号
//按照div的容量分割为小数组 // 按照div的容量分割为小数组
string[][] li = Enumerable.Range(0, files.Count() / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray(); var li = Enumerable.Range(0, files.Length / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray();
foreach (var items in li) foreach (var items in li)
{ {
if (items.Count() == 0) if (items.Length == 0)
continue; continue;
var output = outputName + index.ToString("0000") + ".ts"; var output = outputName + index.ToString("0000") + ".ts";
CombineMultipleFilesIntoSingleFile(items, output); CombineMultipleFilesIntoSingleFile(items, output);
newFiles.Add(output); newFiles.Add(output);
//合并后删除这些文件 // 合并后删除这些文件
foreach (var item in items) foreach (var item in items)
{ {
File.Delete(item); File.Delete(item);
@ -113,7 +98,7 @@ namespace N_m3u8DL_RE.Util
bool writeDate = true, bool useConcatDemuxer = false, string poster = "", string audioName = "", string title = "", bool writeDate = true, bool useConcatDemuxer = false, string poster = "", string audioName = "", string title = "",
string copyright = "", string comment = "", string encodingTool = "", string recTime = "") string copyright = "", string comment = "", string encodingTool = "", string recTime = "")
{ {
//改为绝对路径 // 改为绝对路径
outputPath = Path.GetFullPath(outputPath); outputPath = Path.GetFullPath(outputPath);
string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime; string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime;
@ -186,19 +171,19 @@ namespace N_m3u8DL_RE.Util
return code == 0; return code == 0;
} }
public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath, bool mp4, bool dateinfo) public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath, MuxFormat muxFormat, bool dateinfo)
{ {
var ext = mp4 ? "mp4" : "mkv"; var ext = OtherUtil.GetMuxExtension(muxFormat);
string dateString = DateTime.Now.ToString("o"); string dateString = DateTime.Now.ToString("o");
StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn "); StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn ");
//INPUT // INPUT
foreach (var item in files) foreach (var item in files)
{ {
command.Append($" -i \"{item.FilePath}\" "); command.Append($" -i \"{item.FilePath}\" ");
} }
//MAP // MAP
for (int i = 0; i < files.Length; i++) for (int i = 0; i < files.Length; i++)
{ {
command.Append($" -map {i} "); command.Append($" -map {i} ");
@ -206,19 +191,22 @@ namespace N_m3u8DL_RE.Util
var srt = files.Any(x => x.FilePath.EndsWith(".srt")); var srt = files.Any(x => x.FilePath.EndsWith(".srt"));
if (mp4) if (muxFormat == MuxFormat.MP4)
command.Append($" -strict unofficial -c:a copy -c:v copy -c:s mov_text "); //mp4不支持vtt/srt字幕必须转换格式 command.Append($" -strict unofficial -c:a copy -c:v copy -c:s mov_text "); // mp4不支持vtt/srt字幕必须转换格式
else else if (muxFormat == MuxFormat.TS)
command.Append($" -strict unofficial -c:a copy -c:v copy ");
else if (muxFormat == MuxFormat.MKV)
command.Append($" -strict unofficial -c:a copy -c:v copy -c:s {(srt ? "srt" : "webvtt")} "); command.Append($" -strict unofficial -c:a copy -c:v copy -c:s {(srt ? "srt" : "webvtt")} ");
else throw new ArgumentException($"unknown format: {muxFormat}");
//CLEAN // CLEAN
command.Append(" -map_metadata -1 "); command.Append(" -map_metadata -1 ");
//LANG and NAME // LANG and NAME
var streamIndex = 0; var streamIndex = 0;
for (int i = 0; i < files.Length; i++) for (int i = 0; i < files.Length; i++)
{ {
//转换语言代码 // 转换语言代码
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]); LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
command.Append($" -metadata:s:{streamIndex} language=\"{files[i].LangCode ?? "und"}\" "); command.Append($" -metadata:s:{streamIndex} language=\"{files[i].LangCode ?? "und"}\" ");
if (!string.IsNullOrEmpty(files[i].Description)) if (!string.IsNullOrEmpty(files[i].Description))
@ -240,11 +228,11 @@ namespace N_m3u8DL_RE.Util
var audioTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO); var audioTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO);
var subTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO); var subTracks = files.Where(x => x.MediaType == Common.Enum.MediaType.AUDIO);
if (videoTracks.Any()) command.Append(" -disposition:v:0 default "); if (videoTracks.Any()) command.Append(" -disposition:v:0 default ");
//字幕都不设置默认 // 字幕都不设置默认
if (subTracks.Any()) command.Append(" -disposition:s 0 "); if (subTracks.Any()) command.Append(" -disposition:s 0 ");
if (audioTracks.Any()) if (audioTracks.Any())
{ {
//音频除了第一个音轨 都不设置默认 // 音频除了第一个音轨 都不设置默认
command.Append(" -disposition:a:0 default "); command.Append(" -disposition:a:0 default ");
for (int i = 1; i < audioTracks.Count(); i++) for (int i = 1; i < audioTracks.Count(); i++)
{ {
@ -254,7 +242,7 @@ namespace N_m3u8DL_RE.Util
if (dateinfo) command.Append($" -metadata date=\"{dateString}\" "); if (dateinfo) command.Append($" -metadata date=\"{dateString}\" ");
command.Append($" -ignore_unknown -copy_unknown "); command.Append($" -ignore_unknown -copy_unknown ");
command.Append($" \"{outputPath}.{ext}\""); command.Append($" \"{outputPath}{ext}\"");
var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory); var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory);
@ -269,16 +257,16 @@ namespace N_m3u8DL_RE.Util
var dFlag = false; var dFlag = false;
//LANG and NAME // LANG and NAME
for (int i = 0; i < files.Length; i++) for (int i = 0; i < files.Length; i++)
{ {
//转换语言代码 // 转换语言代码
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]); LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
command.Append($" --language 0:\"{files[i].LangCode ?? "und"}\" "); command.Append($" --language 0:\"{files[i].LangCode ?? "und"}\" ");
//字幕都不设置默认 // 字幕都不设置默认
if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES) if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES)
command.Append($" --default-track 0:no "); command.Append($" --default-track 0:no ");
//音频除了第一个音轨 都不设置默认 // 音频除了第一个音轨 都不设置默认
if (files[i].MediaType == Common.Enum.MediaType.AUDIO) if (files[i].MediaType == Common.Enum.MediaType.AUDIO)
{ {
if (dFlag) if (dFlag)
@ -294,5 +282,4 @@ namespace N_m3u8DL_RE.Util
return code == 0; return code == 0;
} }
}
} }

View File

@ -1,21 +1,16 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Enum;
using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Enum;
using System.CommandLine;
using System.IO.Compression; using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static partial class OtherUtil
{ {
internal class OtherUtil
{
public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers) public static Dictionary<string, string> SplitHeaderArrayToDic(string[]? headers)
{ {
Dictionary<string, string> dic = new(); Dictionary<string, string> dic = new();
if (headers == null) return dic;
if (headers != null)
{
foreach (string header in headers) foreach (string header in headers)
{ {
var index = header.IndexOf(':'); var index = header.IndexOf(':');
@ -24,20 +19,15 @@ namespace N_m3u8DL_RE.Util
dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim(); dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim();
} }
} }
}
return dic; return dic;
} }
private static 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" private static readonly char[] InvalidChars = "34,60,62,124,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,58,42,63,92,47"
.Split(',').Select(s => (char)int.Parse(s)).ToArray(); .Split(',').Select(s => (char)int.Parse(s)).ToArray();
public static string GetValidFileName(string input, string re = ".", bool filterSlash = false) public static string GetValidFileName(string input, string re = "_", bool filterSlash = false)
{ {
string title = input; var title = InvalidChars.Aggregate(input, (current, invalidChar) => current.Replace(invalidChar.ToString(), re));
foreach (char invalidChar in InvalidChars)
{
title = title.Replace(invalidChar.ToString(), re);
}
if (filterSlash) if (filterSlash)
{ {
title = title.Replace("/", re); title = title.Replace("/", re);
@ -50,6 +40,7 @@ namespace N_m3u8DL_RE.Util
/// 从输入自动获取文件名 /// 从输入自动获取文件名
/// </summary> /// </summary>
/// <param name="input"></param> /// <param name="input"></param>
/// <param name="addSuffix"></param>
/// <returns></returns> /// <returns></returns>
public static string GetFileNameFromInput(string input, bool addSuffix = true) public static string GetFileNameFromInput(string input, bool addSuffix = true)
{ {
@ -103,7 +94,7 @@ namespace N_m3u8DL_RE.Util
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
public static double ParseSeconds(string timeStr) public static double ParseSeconds(string timeStr)
{ {
var pattern = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$"); var pattern = TimeStrRegex();
var match = pattern.Match(timeStr); var match = pattern.Match(timeStr);
@ -119,7 +110,7 @@ namespace N_m3u8DL_RE.Util
return hours * 3600 + minutes * 60 + seconds; return hours * 3600 + minutes * 60 + seconds;
} }
//若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录 // 若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录
public static void SafeDeleteDir(string dirPath) public static void SafeDeleteDir(string dirPath)
{ {
if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath)) if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath))
@ -143,15 +134,15 @@ namespace N_m3u8DL_RE.Util
/// <param name="filePath"></param> /// <param name="filePath"></param>
public static async Task DeGzipFileAsync(string filePath) public static async Task DeGzipFileAsync(string filePath)
{ {
string deGzipFile = Path.ChangeExtension(filePath, ".tmp"); var deGzipFile = Path.ChangeExtension(filePath, ".dezip_tmp");
try try
{ {
using (var fileToDecompressAsStream = File.OpenRead(filePath)) await using (var fileToDecompressAsStream = File.OpenRead(filePath))
{ {
using var decompressedStream = File.Create(deGzipFile); await using var decompressedStream = File.Create(deGzipFile);
using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress); await using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress);
await decompressionStream.CopyToAsync(decompressedStream); await decompressionStream.CopyToAsync(decompressedStream);
} };
File.Delete(filePath); File.Delete(filePath);
File.Move(deGzipFile, filePath); File.Move(deGzipFile, filePath);
} }
@ -165,5 +156,18 @@ namespace N_m3u8DL_RE.Util
{ {
return Environment.GetEnvironmentVariable(key) ?? defaultValue; return Environment.GetEnvironmentVariable(key) ?? defaultValue;
} }
public static string GetMuxExtension(MuxFormat muxFormat)
{
return muxFormat switch
{
MuxFormat.MP4 => ".mp4",
MuxFormat.MKV => ".mkv",
MuxFormat.TS => ".ts",
_ => throw new ArgumentException($"unknown format: {muxFormat}")
};
} }
[GeneratedRegex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$")]
private static partial Regex TimeStrRegex();
} }

View File

@ -1,27 +1,20 @@
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource;
using Spectre.Console; using Spectre.Console;
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Diagnostics; using System.Diagnostics;
using System.IO.Pipes; using System.IO.Pipes;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static class PipeUtil
{ {
internal class PipeUtil
{
public static Stream CreatePipe(string pipeName) public static Stream CreatePipe(string pipeName)
{ {
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{ {
return new NamedPipeServerStream(pipeName, PipeDirection.InOut); return new NamedPipeServerStream(pipeName, PipeDirection.InOut);
} }
else
{
var path = Path.Combine(Path.GetTempPath(), pipeName); var path = Path.Combine(Path.GetTempPath(), pipeName);
using var p = new Process(); using var p = new Process();
p.StartInfo = new ProcessStartInfo() p.StartInfo = new ProcessStartInfo()
@ -38,7 +31,6 @@ namespace N_m3u8DL_RE.Util
Thread.Sleep(200); Thread.Sleep(200);
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
} }
}
public static async Task<bool> StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath) public static async Task<bool> StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath)
{ {
@ -67,7 +59,7 @@ namespace N_m3u8DL_RE.Util
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
command.Append($" -i \"\\\\.\\pipe\\{item}\" "); command.Append($" -i \"\\\\.\\pipe\\{item}\" ");
else else
//command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" "); // command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" ");
command.Append($" -i \"{Path.Combine(pipeDir, item)}\" "); command.Append($" -i \"{Path.Combine(pipeDir, item)}\" ");
} }
@ -83,7 +75,7 @@ namespace N_m3u8DL_RE.Util
if (!string.IsNullOrEmpty(customDest)) if (!string.IsNullOrEmpty(customDest))
{ {
if (customDest.Trim().StartsWith("-")) if (customDest.Trim().StartsWith('-'))
command.Append(customDest); command.Append(customDest);
else else
command.Append($" -f mpegts -shortest \"{customDest}\""); command.Append($" -f mpegts -shortest \"{customDest}\"");
@ -103,11 +95,10 @@ namespace N_m3u8DL_RE.Util
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false UseShellExecute = false
}; };
//p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42"); // p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42");
p.Start(); p.Start();
p.WaitForExit(); p.WaitForExit();
return p.ExitCode == 0; return p.ExitCode == 0;
} }
}
} }

View File

@ -1,40 +1,32 @@
using N_m3u8DL_RE.Common.Entity; using N_m3u8DL_RE.Common.Entity;
using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Common.Log;
using N_m3u8DL_RE.Common.Resource; using N_m3u8DL_RE.Common.Resource;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace N_m3u8DL_RE.Util namespace N_m3u8DL_RE.Util;
internal static class SubtitleUtil
{ {
internal class SubtitleUtil
{
/// <summary> /// <summary>
/// 写出图形字幕PNG文件 /// 写出图形字幕PNG文件
/// </summary> /// </summary>
/// <param name="finalVtt"></param> /// <param name="finalVtt"></param>
/// <param name="tmpDir">临时目录</param> /// <param name="tmpDir">临时目录</param>
/// <returns></returns> /// <returns></returns>
public static async Task<bool> TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir) public static async Task TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir)
{ {
if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::"))) if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::")))
{ {
Logger.WarnMarkUp(ResString.processImageSub); Logger.WarnMarkUp(ResString.processImageSub);
var _i = 0; var i = 0;
foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::"))) foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::")))
{ {
var name = $"{_i++}.png"; var name = $"{i++}.png";
var dest = ""; var dest = "";
for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{_i++}.png") ; for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{i++}.png") ;
var base64 = img.Payload[8..]; var base64 = img.Payload[8..];
await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64)); await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64));
img.Payload = name; img.Payload = name;
} }
return true;
}
else return false;
} }
} }
} }