mirror of
https://github.com/NohamR/N_m3u8DL-RE.git
synced 2025-05-24 00:48:58 +00:00
Compare commits
44 Commits
6b181338c9
...
948f7aa75a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
948f7aa75a | ||
![]() |
cd4dfb5e75 | ||
![]() |
f47a0722cf | ||
![]() |
ea9de55eac | ||
![]() |
87914a30cc | ||
![]() |
e350ab7233 | ||
![]() |
4c4617b780 | ||
![]() |
7a54c54786 | ||
![]() |
9f530f2cf6 | ||
![]() |
a8646eb7e7 | ||
![]() |
800ce3d615 | ||
![]() |
49d37c9f14 | ||
![]() |
117c73f54b | ||
![]() |
b044cdb305 | ||
![]() |
adbe376ae0 | ||
![]() |
cacf9b0ff0 | ||
![]() |
bc8b5a92a9 | ||
![]() |
7463915fbb | ||
![]() |
8702d36276 | ||
![]() |
3081701a32 | ||
![]() |
77fdaaf9bd | ||
![]() |
8095c6e172 | ||
![]() |
6bd906e4e6 | ||
![]() |
09d9f0e320 | ||
![]() |
9752df8aae | ||
![]() |
b3d95963db | ||
![]() |
312325ca18 | ||
![]() |
e8e92b6337 | ||
![]() |
0c73b730bb | ||
![]() |
c004a1c72f | ||
![]() |
bb20d50122 | ||
![]() |
3cd3bb9516 | ||
![]() |
5a56e34cd5 | ||
![]() |
f6ad09255e | ||
![]() |
b9d3b57b39 | ||
![]() |
7d8e7c6402 | ||
![]() |
9fc37d5b61 | ||
![]() |
8a25815c1f | ||
![]() |
dd30bd99f9 | ||
![]() |
9c49fce4ff | ||
![]() |
6e92acfda9 | ||
![]() |
d1ffac817d | ||
![]() |
164f2cb59b | ||
![]() |
2bf4f29f28 |
328
.github/workflows/build_latest.yml
vendored
328
.github/workflows/build_latest.yml
vendored
@ -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 }}
|
|
132
README.md
132
README.md
@ -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]
|
||||||
@ -34,69 +34,73 @@ Arguments:
|
|||||||
<input> 链接或文件
|
<input> 链接或文件
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--tmp-dir <tmp-dir> 设置临时文件存储目录
|
--tmp-dir <tmp-dir> 设置临时文件存储目录
|
||||||
--save-dir <save-dir> 设置输出目录
|
--save-dir <save-dir> 设置输出目录
|
||||||
--save-name <save-name> 设置保存文件名
|
--save-name <save-name> 设置保存文件名
|
||||||
--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]
|
||||||
--force-ansi-console 强制认定终端为支持ANSI且可交互的终端
|
--http-request-timeout <seconds> HTTP请求的超时时间(秒) [default: 100]
|
||||||
--no-ansi-color 去除ANSI颜色
|
--force-ansi-console 强制认定终端为支持ANSI且可交互的终端
|
||||||
--auto-select 自动选择所有类型的最佳轨道 [default: False]
|
--no-ansi-color 去除ANSI颜色
|
||||||
--skip-merge 跳过合并分片 [default: False]
|
--auto-select 自动选择所有类型的最佳轨道 [default: False]
|
||||||
--skip-download 跳过下载 [default: False]
|
--skip-merge 跳过合并分片 [default: False]
|
||||||
--check-segments-count 检测实际下载的分片数量和预期数量是否匹配 [default: True]
|
--skip-download 跳过下载 [default: False]
|
||||||
--binary-merge 二进制合并 [default: False]
|
--check-segments-count 检测实际下载的分片数量和预期数量是否匹配 [default: True]
|
||||||
--use-ffmpeg-concat-demuxer 使用 ffmpeg 合并时,使用 concat 分离器而非 concat 协议 [default: False]
|
--binary-merge 二进制合并 [default: False]
|
||||||
--del-after-done 完成后删除临时文件 [default: True]
|
--use-ffmpeg-concat-demuxer 使用 ffmpeg 合并时,使用 concat 分离器而非 concat 协议 [default: False]
|
||||||
--no-date-info 混流时不写入日期信息 [default: False]
|
--del-after-done 完成后删除临时文件 [default: True]
|
||||||
--no-log 关闭日志文件输出 [default: False]
|
--no-date-info 混流时不写入日期信息 [default: False]
|
||||||
--write-meta-json 解析后的信息是否输出json文件 [default: True]
|
--no-log 关闭日志文件输出 [default: False]
|
||||||
--append-url-params 将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com [default: False]
|
--write-meta-json 解析后的信息是否输出json文件 [default: True]
|
||||||
-mt, --concurrent-download 并发下载已选择的音频、视频和字幕 [default: False]
|
--append-url-params 将输入Url的Params添加至分片, 对某些网站很有用, 例如 kakao.com [default: False]
|
||||||
-H, --header <header> 为HTTP请求设置特定的请求头, 例如:
|
-mt, --concurrent-download 并发下载已选择的音频、视频和字幕 [default: False]
|
||||||
-H "Cookie: mycookie" -H "User-Agent: iOS"
|
-H, --header <header> 为HTTP请求设置特定的请求头, 例如:
|
||||||
--sub-only 只选取字幕轨道 [default: False]
|
-H "Cookie: mycookie" -H "User-Agent: iOS"
|
||||||
--sub-format <SRT|VTT> 字幕输出类型 [default: SRT]
|
--sub-only 只选取字幕轨道 [default: False]
|
||||||
--auto-subtitle-fix 自动修正字幕 [default: True]
|
--sub-format <SRT|VTT> 字幕输出类型 [default: SRT]
|
||||||
--ffmpeg-binary-path <PATH> ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe
|
--auto-subtitle-fix 自动修正字幕 [default: True]
|
||||||
--log-level <DEBUG|ERROR|INFO|OFF|WARN> 设置日志级别 [default: INFO]
|
--ffmpeg-binary-path <PATH> ffmpeg可执行程序全路径, 例如 C:\Tools\ffmpeg.exe
|
||||||
--ui-language <en-US|zh-CN|zh-TW> 设置UI语言
|
--log-level <DEBUG|ERROR|INFO|OFF|WARN> 设置日志级别 [default: INFO]
|
||||||
--urlprocessor-args <urlprocessor-args> 此字符串将直接传递给URL Processor
|
--ui-language <en-US|zh-CN|zh-TW> 设置UI语言
|
||||||
--key <key> 设置解密密钥, 程序调用mp4decrpyt/shaka-packager进行解密. 格式:
|
--urlprocessor-args <urlprocessor-args> 此字符串将直接传递给URL Processor
|
||||||
--key KID1:KEY1 --key KID2:KEY2
|
--key <key> 设置解密密钥, 程序调用mp4decrpyt/shaka-packager/ffmpeg进行解密. 格式:
|
||||||
--key-text-file <key-text-file> 设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)
|
--key KID1:KEY1 --key KID2:KEY2
|
||||||
--decryption-binary-path <PATH> MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe
|
对于KEY相同的情况可以直接输入 --key KEY
|
||||||
--use-shaka-packager 解密时使用shaka-packager替代mp4decrypt [default: False]
|
--key-text-file <key-text-file> 设置密钥文件,程序将从文件中按KID搜寻KEY以解密.(不建议使用特大文件)
|
||||||
--mp4-real-time-decryption 实时解密MP4分片 [default: False]
|
--decryption-engine <FFMPEG|MP4DECRYPT|SHAKA_PACKAGER> 设置解密时使用的第三方程序 [default: MP4DECRYPT]
|
||||||
-R, --max-speed <SPEED> 设置限速,单位支持 Mbps 或 Kbps,如:15M 100K
|
--decryption-binary-path <PATH> MP4解密所用工具的全路径, 例如 C:\Tools\mp4decrypt.exe
|
||||||
-M, --mux-after-done <OPTIONS> 所有工作完成时尝试混流分离的音视频. 输入 "--morehelp mux-after-done" 以查看详细信息
|
--mp4-real-time-decryption 实时解密MP4分片 [default: False]
|
||||||
--custom-hls-method <METHOD> 指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)
|
-R, --max-speed <SPEED> 设置限速,单位支持 Mbps 或 Kbps,如:15M 100K
|
||||||
--custom-hls-key <FILE|HEX|BASE64> 指定HLS解密KEY. 可以是文件, HEX或Base64
|
-M, --mux-after-done <OPTIONS> 所有工作完成时尝试混流分离的音视频. 输入 "--morehelp mux-after-done" 以查看详细信息
|
||||||
--custom-hls-iv <FILE|HEX|BASE64> 指定HLS解密IV. 可以是文件, HEX或Base64
|
--custom-hls-method <METHOD> 指定HLS加密方式 (AES_128|AES_128_ECB|CENC|CHACHA20|NONE|SAMPLE_AES|SAMPLE_AES_CTR|UNKNOWN)
|
||||||
--use-system-proxy 使用系统默认代理 [default: True]
|
--custom-hls-key <FILE|HEX|BASE64> 指定HLS解密KEY. 可以是文件, HEX或Base64
|
||||||
--custom-proxy <URL> 设置请求代理, 如 http://127.0.0.1:8888
|
--custom-hls-iv <FILE|HEX|BASE64> 指定HLS解密IV. 可以是文件, HEX或Base64
|
||||||
--custom-range <RANGE> 仅下载部分分片. 输入 "--morehelp custom-range" 以查看详细信息
|
--use-system-proxy 使用系统默认代理 [default: True]
|
||||||
--task-start-at <yyyyMMddHHmmss> 在此时间之前不会开始执行任务
|
--custom-proxy <URL> 设置请求代理, 如 http://127.0.0.1:8888
|
||||||
--live-perform-as-vod 以点播方式下载直播流 [default: False]
|
--custom-range <RANGE> 仅下载部分分片. 输入 "--morehelp custom-range" 以查看详细信息
|
||||||
--live-real-time-merge 录制直播时实时合并 [default: False]
|
--task-start-at <yyyyMMddHHmmss> 在此时间之前不会开始执行任务
|
||||||
--live-keep-segments 录制直播并开启实时合并时依然保留分片 [default: True]
|
--live-perform-as-vod 以点播方式下载直播流 [default: False]
|
||||||
--live-pipe-mux 录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件 [default: False]
|
--live-real-time-merge 录制直播时实时合并 [default: False]
|
||||||
--live-fix-vtt-by-audio 通过读取音频文件的起始时间修正VTT字幕 [default: False]
|
--live-keep-segments 录制直播并开启实时合并时依然保留分片 [default: True]
|
||||||
--live-record-limit <HH:mm:ss> 录制直播时的录制时长限制
|
--live-pipe-mux 录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件 [default: False]
|
||||||
--live-wait-time <SEC> 手动设置直播列表刷新间隔
|
--live-fix-vtt-by-audio 通过读取音频文件的起始时间修正VTT字幕 [default: False]
|
||||||
--live-take-count <NUM> 手动设置录制直播时首次获取分片的数量 [default: 16]
|
--live-record-limit <HH:mm:ss> 录制直播时的录制时长限制
|
||||||
--mux-import <OPTIONS> 混流时引入外部媒体文件. 输入 "--morehelp mux-import" 以查看详细信息
|
--live-wait-time <SEC> 手动设置直播列表刷新间隔
|
||||||
-sv, --select-video <OPTIONS> 通过正则表达式选择符合要求的视频流. 输入 "--morehelp select-video" 以查看详细信息
|
--live-take-count <NUM> 手动设置录制直播时首次获取分片的数量 [default: 16]
|
||||||
-sa, --select-audio <OPTIONS> 通过正则表达式选择符合要求的音频流. 输入 "--morehelp select-audio" 以查看详细信息
|
--mux-import <OPTIONS> 混流时引入外部媒体文件. 输入 "--morehelp mux-import" 以查看详细信息
|
||||||
-ss, --select-subtitle <OPTIONS> 通过正则表达式选择符合要求的字幕流. 输入 "--morehelp select-subtitle" 以查看详细信息
|
-sv, --select-video <OPTIONS> 通过正则表达式选择符合要求的视频流. 输入 "--morehelp select-video" 以查看详细信息
|
||||||
-dv, --drop-video <OPTIONS> 通过正则表达式去除符合要求的视频流.
|
-sa, --select-audio <OPTIONS> 通过正则表达式选择符合要求的音频流. 输入 "--morehelp select-audio" 以查看详细信息
|
||||||
-da, --drop-audio <OPTIONS> 通过正则表达式去除符合要求的音频流.
|
-ss, --select-subtitle <OPTIONS> 通过正则表达式选择符合要求的字幕流. 输入 "--morehelp select-subtitle" 以查看详细信息
|
||||||
-ds, --drop-subtitle <OPTIONS> 通过正则表达式去除符合要求的字幕流.
|
-dv, --drop-video <OPTIONS> 通过正则表达式去除符合要求的视频流.
|
||||||
--ad-keyword <REG> 设置广告分片的URL关键字(正则表达式)
|
-da, --drop-audio <OPTIONS> 通过正则表达式去除符合要求的音频流.
|
||||||
--morehelp <OPTION> 查看某个选项的详细帮助信息
|
-ds, --drop-subtitle <OPTIONS> 通过正则表达式去除符合要求的字幕流.
|
||||||
--version Show version information
|
--ad-keyword <REG> 设置广告分片的URL关键字(正则表达式)
|
||||||
-?, -h, --help Show help and usage information
|
--disable-update-check 禁用版本更新检测 [default: False]
|
||||||
|
--allow-hls-multi-ext-map 允许HLS中的多个#EXT-X-MAP(实验性) [default: False]
|
||||||
|
--morehelp <OPTION> 查看某个选项的详细帮助信息
|
||||||
|
--version Show version information
|
||||||
|
-?, -h, --help Show help and usage information
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -1,43 +1,34 @@
|
|||||||
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>
|
||||||
|
public EncryptMethod Method { get; set; } = EncryptMethod.NONE;
|
||||||
|
|
||||||
|
public byte[]? Key { get; set; }
|
||||||
|
public byte[]? IV { get; set; }
|
||||||
|
|
||||||
|
public EncryptInfo() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建EncryptInfo并尝试自动解析Method
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method"></param>
|
||||||
|
public EncryptInfo(string method)
|
||||||
{
|
{
|
||||||
/// <summary>
|
Method = ParseMethod(method);
|
||||||
/// 加密方式,默认无加密
|
|
||||||
/// </summary>
|
|
||||||
public EncryptMethod Method { get; set; } = EncryptMethod.NONE;
|
|
||||||
|
|
||||||
public byte[]? Key { get; set; }
|
|
||||||
public byte[]? IV { get; set; }
|
|
||||||
|
|
||||||
public EncryptInfo() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建EncryptInfo并尝试自动解析Method
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="method"></param>
|
|
||||||
public EncryptInfo(string method)
|
|
||||||
{
|
|
||||||
Method = ParseMethod(method);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static EncryptMethod ParseMethod(string? method)
|
|
||||||
{
|
|
||||||
if (method != null && System.Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m))
|
|
||||||
{
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return EncryptMethod.UNKNOWN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static EncryptMethod ParseMethod(string? method)
|
||||||
|
{
|
||||||
|
if (method != null && System.Enum.TryParse(method.Replace("-", "_"), out EncryptMethod m))
|
||||||
|
{
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
return EncryptMethod.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,18 @@
|
|||||||
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 CodecPrivateData { get; set; } = "";
|
||||||
public string FourCC { get; set; } = "";
|
public string Type { get; set; } = "";
|
||||||
public string CodecPrivateData { get; set; } = "";
|
public int Timesacle { get; set; }
|
||||||
public string Type { get; set; } = "";
|
public int SamplingRate { get; set; }
|
||||||
public int Timesacle { get; set; }
|
public int Channels { get; set; }
|
||||||
public int SamplingRate { get; set; }
|
public int BitsPerSample { get; set; }
|
||||||
public int Channels { get; set; }
|
public int NalUnitLengthField { get; set; }
|
||||||
public int BitsPerSample { get; set; }
|
public long Duration { get; set; }
|
||||||
public int NalUnitLengthField { get; set; }
|
|
||||||
public long Duration { get; set; }
|
|
||||||
|
|
||||||
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; } = "";
|
||||||
}
|
}
|
||||||
}
|
|
@ -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>();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +1,40 @@
|
|||||||
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 double Duration { get; set; }
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public DateTime? DateTime { get; set; }
|
||||||
|
|
||||||
|
public long? StartRange { get; set; }
|
||||||
|
public long? StopRange => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null;
|
||||||
|
public long? ExpectLength { get; set; }
|
||||||
|
|
||||||
|
public EncryptInfo EncryptInfo { get; set; } = new();
|
||||||
|
|
||||||
|
public bool IsEncrypted => EncryptInfo.Method != EncryptMethod.NONE;
|
||||||
|
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? NameFromVar { get; set; } // MPD分段文件名
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
public long Index { get; set; }
|
return obj is MediaSegment segment &&
|
||||||
public double Duration { get; set; }
|
Index == segment.Index &&
|
||||||
public string? Title { get; set; }
|
Math.Abs(Duration - segment.Duration) < 0.001 &&
|
||||||
public DateTime? DateTime { get; set; }
|
Title == segment.Title &&
|
||||||
|
StartRange == segment.StartRange &&
|
||||||
public long? StartRange { get; set; }
|
StopRange == segment.StopRange &&
|
||||||
public long? StopRange { get => (StartRange != null && ExpectLength != null) ? StartRange + ExpectLength - 1 : null; }
|
ExpectLength == segment.ExpectLength &&
|
||||||
public long? ExpectLength { get; set; }
|
Url == segment.Url;
|
||||||
|
|
||||||
public EncryptInfo EncryptInfo { get; set; } = new EncryptInfo();
|
|
||||||
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
public string? NameFromVar { get; set; } //MPD分段文件名
|
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
return obj is MediaSegment segment &&
|
|
||||||
Index == segment.Index &&
|
|
||||||
Duration == segment.Duration &&
|
|
||||||
Title == segment.Title &&
|
|
||||||
StartRange == segment.StartRange &&
|
|
||||||
StopRange == segment.StopRange &&
|
|
||||||
ExpectLength == segment.ExpectLength &&
|
|
||||||
Url == segment.Url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(Index, Duration, Title, StartRange, StopRange, ExpectLength, Url);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
//是否直播
|
// 直播刷新间隔毫秒(默认15秒)
|
||||||
public bool IsLive { get; set; } = false;
|
public double RefreshIntervalMs { get; set; } = 15000;
|
||||||
//直播刷新间隔毫秒(默认15秒)
|
// 所有分片时长总和
|
||||||
public double RefreshIntervalMs { get; set; } = 15000;
|
public double TotalDuration => MediaParts.Sum(x => x.MediaSegments.Sum(m => m.Duration));
|
||||||
//所有分片时长总和
|
|
||||||
public double TotalDuration { get => 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; } = [];
|
||||||
public List<MediaPart> MediaParts { get; set; } = new List<MediaPart>();
|
}
|
||||||
}
|
|
||||||
}
|
|
@ -1,188 +1,182 @@
|
|||||||
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 string? GroupId { get; set; }
|
||||||
|
public string? Language { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public Choise? Default { get; set; }
|
||||||
|
|
||||||
|
// 由于用户选择 被跳过的分片总时长
|
||||||
|
public double? SkippedDuration { get; set; }
|
||||||
|
|
||||||
|
// MSS信息
|
||||||
|
public MSSData? MSSData { get; set; }
|
||||||
|
|
||||||
|
// 基本信息
|
||||||
|
public int? Bandwidth { get; set; }
|
||||||
|
public string? Codecs { get; set; }
|
||||||
|
public string? Resolution { get; set; }
|
||||||
|
public double? FrameRate { get; set; }
|
||||||
|
public string? Channels { get; set; }
|
||||||
|
public string? Extension { get; set; }
|
||||||
|
|
||||||
|
// Dash
|
||||||
|
public RoleType? Role { get; set; }
|
||||||
|
|
||||||
|
// 补充信息-色域
|
||||||
|
public string? VideoRange { get; set; }
|
||||||
|
// 补充信息-特征
|
||||||
|
public string? Characteristics { get; set; }
|
||||||
|
// 发布时间(仅MPD需要)
|
||||||
|
public DateTime? PublishTime { get; set; }
|
||||||
|
|
||||||
|
// 外部轨道GroupId (后续寻找对应轨道信息)
|
||||||
|
public string? AudioId { get; set; }
|
||||||
|
public string? VideoId { get; set; }
|
||||||
|
public string? SubtitleId { get; set; }
|
||||||
|
|
||||||
|
public string? PeriodId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始URL
|
||||||
|
/// </summary>
|
||||||
|
public string OriginalUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public Playlist? Playlist { get; set; }
|
||||||
|
|
||||||
|
public int SegmentsCount
|
||||||
{
|
{
|
||||||
public MediaType? MediaType { get; set; }
|
get
|
||||||
public string? GroupId { get; set; }
|
|
||||||
public string? Language { get; set; }
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public Choise? Default { get; set; }
|
|
||||||
|
|
||||||
//由于用户选择 被跳过的分片总时长
|
|
||||||
public double? SkippedDuration { get; set; }
|
|
||||||
|
|
||||||
//MSS信息
|
|
||||||
public MSSData? MSSData { get; set; }
|
|
||||||
|
|
||||||
//基本信息
|
|
||||||
public int? Bandwidth { get; set; }
|
|
||||||
public string? Codecs { get; set; }
|
|
||||||
public string? Resolution { get; set; }
|
|
||||||
public double? FrameRate { get; set; }
|
|
||||||
public string? Channels { get; set; }
|
|
||||||
public string? Extension { get; set; }
|
|
||||||
|
|
||||||
//Dash
|
|
||||||
public RoleType? Role { get; set; }
|
|
||||||
|
|
||||||
//补充信息-色域
|
|
||||||
public string? VideoRange { get; set; }
|
|
||||||
//补充信息-特征
|
|
||||||
public string? Characteristics { get; set; }
|
|
||||||
//发布时间(仅MPD需要)
|
|
||||||
public DateTime? PublishTime { get; set; }
|
|
||||||
|
|
||||||
//外部轨道GroupId (后续寻找对应轨道信息)
|
|
||||||
public string? AudioId { get; set; }
|
|
||||||
public string? VideoId { get; set; }
|
|
||||||
public string? SubtitleId { get; set; }
|
|
||||||
|
|
||||||
public string? PeriodId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// URL
|
|
||||||
/// </summary>
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 原始URL
|
|
||||||
/// </summary>
|
|
||||||
public string OriginalUrl { get; set; }
|
|
||||||
|
|
||||||
public Playlist? Playlist { get; set; }
|
|
||||||
|
|
||||||
public int SegmentsCount
|
|
||||||
{
|
{
|
||||||
get
|
return Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) : 0;
|
||||||
{
|
|
||||||
return Playlist != null ? Playlist.MediaParts.Sum(x => x.MediaSegments.Count) : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToShortString()
|
|
||||||
{
|
|
||||||
var prefixStr = "";
|
|
||||||
var returnStr = "";
|
|
||||||
var encStr = string.Empty;
|
|
||||||
|
|
||||||
if (MediaType == Enum.MediaType.AUDIO)
|
|
||||||
{
|
|
||||||
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
|
|
||||||
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
else if (MediaType == Enum.MediaType.SUBTITLES)
|
|
||||||
{
|
|
||||||
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
|
|
||||||
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
prefixStr = $"[aqua]Vid[/] {encStr}";
|
|
||||||
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
|
|
||||||
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
|
|
||||||
while (returnStr.Contains("| |"))
|
|
||||||
{
|
|
||||||
returnStr = returnStr.Replace("| |", "|");
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToShortShortString()
|
|
||||||
{
|
|
||||||
var prefixStr = "";
|
|
||||||
var returnStr = "";
|
|
||||||
var encStr = string.Empty;
|
|
||||||
|
|
||||||
if (MediaType == Enum.MediaType.AUDIO)
|
|
||||||
{
|
|
||||||
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
|
|
||||||
var d = $"{(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
else if (MediaType == Enum.MediaType.SUBTITLES)
|
|
||||||
{
|
|
||||||
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
|
|
||||||
var d = $"{Language} | {Name} | {Codecs} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
prefixStr = $"[aqua]Vid[/] {encStr}";
|
|
||||||
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {FrameRate} | {VideoRange} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
|
|
||||||
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
|
|
||||||
while (returnStr.Contains("| |"))
|
|
||||||
{
|
|
||||||
returnStr = returnStr.Replace("| |", "|");
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
var prefixStr = "";
|
|
||||||
var returnStr = "";
|
|
||||||
var encStr = string.Empty;
|
|
||||||
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)))
|
|
||||||
{
|
|
||||||
var ms = Playlist.MediaParts.SelectMany(m => m.MediaSegments.Select(s => s.EncryptInfo.Method)).Where(e => e != EncryptMethod.NONE).Distinct();
|
|
||||||
encStr = $"[red]*{string.Join(",", ms).EscapeMarkup()}[/] ";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MediaType == Enum.MediaType.AUDIO)
|
|
||||||
{
|
|
||||||
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
|
|
||||||
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {segmentsCountStr} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
else if (MediaType == Enum.MediaType.SUBTITLES)
|
|
||||||
{
|
|
||||||
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
|
|
||||||
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Characteristics} | {segmentsCountStr} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
prefixStr = $"[aqua]Vid[/] {encStr}";
|
|
||||||
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {segmentsCountStr} | {Role}";
|
|
||||||
returnStr = d.EscapeMarkup();
|
|
||||||
}
|
|
||||||
|
|
||||||
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
|
|
||||||
while (returnStr.Contains("| |"))
|
|
||||||
{
|
|
||||||
returnStr = returnStr.Replace("| |", "|");
|
|
||||||
}
|
|
||||||
|
|
||||||
//计算时长
|
|
||||||
if (Playlist != null)
|
|
||||||
{
|
|
||||||
var total = Playlist.TotalDuration;
|
|
||||||
returnStr += " | ~" + GlobalUtil.FormatTime((int)total);
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public string ToShortString()
|
||||||
|
{
|
||||||
|
var prefixStr = "";
|
||||||
|
var returnStr = "";
|
||||||
|
var encStr = string.Empty;
|
||||||
|
|
||||||
|
if (MediaType == Enum.MediaType.AUDIO)
|
||||||
|
{
|
||||||
|
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
|
||||||
|
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
else if (MediaType == Enum.MediaType.SUBTITLES)
|
||||||
|
{
|
||||||
|
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
|
||||||
|
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prefixStr = $"[aqua]Vid[/] {encStr}";
|
||||||
|
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
|
||||||
|
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
|
||||||
|
while (returnStr.Contains("| |"))
|
||||||
|
{
|
||||||
|
returnStr = returnStr.Replace("| |", "|");
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToShortShortString()
|
||||||
|
{
|
||||||
|
var prefixStr = "";
|
||||||
|
var returnStr = "";
|
||||||
|
var encStr = string.Empty;
|
||||||
|
|
||||||
|
if (MediaType == Enum.MediaType.AUDIO)
|
||||||
|
{
|
||||||
|
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
|
||||||
|
var d = $"{(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
else if (MediaType == Enum.MediaType.SUBTITLES)
|
||||||
|
{
|
||||||
|
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
|
||||||
|
var d = $"{Language} | {Name} | {Codecs} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prefixStr = $"[aqua]Vid[/] {encStr}";
|
||||||
|
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {FrameRate} | {VideoRange} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
|
||||||
|
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
|
||||||
|
while (returnStr.Contains("| |"))
|
||||||
|
{
|
||||||
|
returnStr = returnStr.Replace("| |", "|");
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var prefixStr = "";
|
||||||
|
var returnStr = "";
|
||||||
|
var encStr = string.Empty;
|
||||||
|
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)))
|
||||||
|
{
|
||||||
|
var ms = Playlist.MediaParts.SelectMany(m => m.MediaSegments.Select(s => s.EncryptInfo.Method)).Where(e => e != EncryptMethod.NONE).Distinct();
|
||||||
|
encStr = $"[red]*{string.Join(",", ms).EscapeMarkup()}[/] ";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MediaType == Enum.MediaType.AUDIO)
|
||||||
|
{
|
||||||
|
prefixStr = $"[deepskyblue3]Aud[/] {encStr}";
|
||||||
|
var d = $"{GroupId} | {(Bandwidth != null ? (Bandwidth / 1000) + " Kbps" : "")} | {Name} | {Codecs} | {Language} | {(Channels != null ? Channels + "CH" : "")} | {segmentsCountStr} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
else if (MediaType == Enum.MediaType.SUBTITLES)
|
||||||
|
{
|
||||||
|
prefixStr = $"[deepskyblue3_1]Sub[/] {encStr}";
|
||||||
|
var d = $"{GroupId} | {Language} | {Name} | {Codecs} | {Characteristics} | {segmentsCountStr} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prefixStr = $"[aqua]Vid[/] {encStr}";
|
||||||
|
var d = $"{Resolution} | {Bandwidth / 1000} Kbps | {GroupId} | {FrameRate} | {Codecs} | {VideoRange} | {segmentsCountStr} | {Role}";
|
||||||
|
returnStr = d.EscapeMarkup();
|
||||||
|
}
|
||||||
|
|
||||||
|
returnStr = prefixStr + returnStr.Trim().Trim('|').Trim();
|
||||||
|
while (returnStr.Contains("| |"))
|
||||||
|
{
|
||||||
|
returnStr = returnStr.Replace("| |", "|");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算时长
|
||||||
|
if (Playlist != null)
|
||||||
|
{
|
||||||
|
var total = Playlist.TotalDuration;
|
||||||
|
returnStr += " | ~" + GlobalUtil.FormatTime((int)total);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnStr.TrimEnd().TrimEnd('|').TrimEnd();
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,23 @@
|
|||||||
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 EndTime { get; set; }
|
||||||
|
public required string Payload { get; set; }
|
||||||
|
public required string Settings { get; set; }
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
public TimeSpan StartTime { get; set; }
|
return obj is SubCue cue &&
|
||||||
public TimeSpan EndTime { get; set; }
|
StartTime.Equals(cue.StartTime) &&
|
||||||
public required string Payload { get; set; }
|
EndTime.Equals(cue.EndTime) &&
|
||||||
public required string Settings { get; set; }
|
Payload == cue.Payload &&
|
||||||
|
Settings == cue.Settings;
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
return obj is SubCue cue &&
|
|
||||||
StartTime.Equals(cue.StartTime) &&
|
|
||||||
EndTime.Equals(cue.EndTime) &&
|
|
||||||
Payload == cue.Payload &&
|
|
||||||
Settings == cue.Settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return HashCode.Combine(StartTime, EndTime, Payload, Settings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return HashCode.Combine(StartTime, EndTime, Payload, Settings);
|
||||||
|
}
|
||||||
|
}
|
@ -1,272 +1,268 @@
|
|||||||
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.*")]
|
||||||
|
private static partial Regex TSMapRegex();
|
||||||
|
[GeneratedRegex("MPEGTS:(\\d+)")]
|
||||||
|
private static partial Regex TSValueRegex();
|
||||||
|
[GeneratedRegex("\\s")]
|
||||||
|
private static partial Regex SplitRegex();
|
||||||
|
[GeneratedRegex(@"<c\..*?>([\s\S]*?)<\/c>")]
|
||||||
|
private static partial Regex VttClassRegex();
|
||||||
|
|
||||||
|
public List<SubCue> Cues { get; set; } = [];
|
||||||
|
public long MpegtsTimestamp { get; set; } = 0L;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从字节数组解析WEBVTT
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="textBytes"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L)
|
||||||
{
|
{
|
||||||
[GeneratedRegex("X-TIMESTAMP-MAP.*")]
|
return Parse(Encoding.UTF8.GetString(textBytes), BaseTimestamp);
|
||||||
private static partial Regex TSMapRegex();
|
}
|
||||||
[GeneratedRegex("MPEGTS:(\\d+)")]
|
|
||||||
private static partial Regex TSValueRegex();
|
|
||||||
[GeneratedRegex("\\s")]
|
|
||||||
private static partial Regex SplitRegex();
|
|
||||||
[GeneratedRegex("<c\\..*?>([\\s\\S]*?)<\\/c>")]
|
|
||||||
private static partial Regex VttClassRegex();
|
|
||||||
|
|
||||||
public List<SubCue> Cues { get; set; } = new List<SubCue>();
|
/// <summary>
|
||||||
public long MpegtsTimestamp { get; set; } = 0L;
|
/// 从字节数组解析WEBVTT
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="textBytes"></param>
|
||||||
|
/// <param name="encoding"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static WebVttSub Parse(byte[] textBytes, Encoding encoding, long BaseTimestamp = 0L)
|
||||||
|
{
|
||||||
|
return Parse(encoding.GetString(textBytes), BaseTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从字节数组解析WEBVTT
|
/// 从字符串解析WEBVTT
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="textBytes"></param>
|
/// <param name="text"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static WebVttSub Parse(byte[] textBytes, long BaseTimestamp = 0L)
|
public static WebVttSub Parse(string text, long BaseTimestamp = 0L)
|
||||||
|
{
|
||||||
|
if (!text.Trim().StartsWith("WEBVTT"))
|
||||||
|
throw new Exception("Bad vtt!");
|
||||||
|
|
||||||
|
text += Environment.NewLine;
|
||||||
|
|
||||||
|
var webSub = new WebVttSub();
|
||||||
|
var needPayload = false;
|
||||||
|
var timeLine = "";
|
||||||
|
var regex1 = TSMapRegex();
|
||||||
|
|
||||||
|
if (regex1.IsMatch(text))
|
||||||
{
|
{
|
||||||
return Parse(Encoding.UTF8.GetString(textBytes), BaseTimestamp);
|
var timestamp = TSValueRegex().Match(regex1.Match(text).Value).Groups[1].Value;
|
||||||
|
webSub.MpegtsTimestamp = Convert.ToInt64(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var payloads = new List<string>();
|
||||||
/// 从字节数组解析WEBVTT
|
foreach (var line in text.Split('\n'))
|
||||||
/// </summary>
|
|
||||||
/// <param name="textBytes"></param>
|
|
||||||
/// <param name="encoding"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static WebVttSub Parse(byte[] textBytes, Encoding encoding, long BaseTimestamp = 0L)
|
|
||||||
{
|
{
|
||||||
return Parse(encoding.GetString(textBytes), BaseTimestamp);
|
if (line.Contains(" --> "))
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从字符串解析WEBVTT
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static WebVttSub Parse(string text, long BaseTimestamp = 0L)
|
|
||||||
{
|
|
||||||
if (!text.Trim().StartsWith("WEBVTT"))
|
|
||||||
throw new Exception("Bad vtt!");
|
|
||||||
|
|
||||||
text += Environment.NewLine;
|
|
||||||
|
|
||||||
var webSub = new WebVttSub();
|
|
||||||
var needPayload = false;
|
|
||||||
var timeLine = "";
|
|
||||||
var regex1 = TSMapRegex();
|
|
||||||
|
|
||||||
if (regex1.IsMatch(text))
|
|
||||||
{
|
{
|
||||||
var timestamp = TSValueRegex().Match(regex1.Match(text).Value).Groups[1].Value;
|
needPayload = true;
|
||||||
webSub.MpegtsTimestamp = Convert.ToInt64(timestamp);
|
timeLine = line.Trim();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var payloads = new List<string>();
|
if (!needPayload) continue;
|
||||||
foreach (var line in text.Split('\n'))
|
|
||||||
|
if (string.IsNullOrEmpty(line.Trim()))
|
||||||
{
|
{
|
||||||
if (line.Contains(" --> "))
|
var payload = string.Join(Environment.NewLine, payloads);
|
||||||
{
|
if (string.IsNullOrEmpty(payload.Trim())) continue; // 没获取到payload 跳过添加
|
||||||
needPayload = true;
|
|
||||||
timeLine = line.Trim();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needPayload)
|
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||||
|
var startTime = ConvertToTS(arr[0]);
|
||||||
|
var endTime = ConvertToTS(arr[1]);
|
||||||
|
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
|
||||||
|
webSub.Cues.Add(new SubCue()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(line.Trim()))
|
StartTime = startTime,
|
||||||
{
|
EndTime = endTime,
|
||||||
var payload = string.Join(Environment.NewLine, payloads);
|
Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), // Remove Zero Width Space!
|
||||||
if (string.IsNullOrEmpty(payload.Trim())) continue; //没获取到payload 跳过添加
|
Settings = style
|
||||||
|
});
|
||||||
|
payloads.Clear();
|
||||||
|
needPayload = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
payloads.Add(line.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var arr = SplitRegex().Split(timeLine.Replace("-->", "")).Where(s => !string.IsNullOrEmpty(s)).ToList();
|
if (BaseTimestamp == 0) return webSub;
|
||||||
var startTime = ConvertToTS(arr[0]);
|
|
||||||
var endTime = ConvertToTS(arr[1]);
|
foreach (var item in webSub.Cues)
|
||||||
var style = arr.Count > 2 ? string.Join(" ", arr.Skip(2)) : "";
|
{
|
||||||
webSub.Cues.Add(new SubCue()
|
if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)
|
||||||
{
|
{
|
||||||
StartTime = startTime,
|
item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp);
|
||||||
EndTime = endTime,
|
item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp);
|
||||||
Payload = RemoveClassTag(string.Join("", payload.Where(c => c != 8203))), //Remove Zero Width Space!
|
}
|
||||||
Settings = style
|
else
|
||||||
});
|
{
|
||||||
payloads.Clear();
|
break;
|
||||||
needPayload = false;
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
return webSub;
|
||||||
payloads.Add(line.Trim());
|
}
|
||||||
}
|
|
||||||
|
private static string RemoveClassTag(string text)
|
||||||
|
{
|
||||||
|
if (VttClassRegex().IsMatch(text))
|
||||||
|
{
|
||||||
|
return string.Join(Environment.NewLine, text.Split('\n').Select(line => line.TrimEnd()).Select(line =>
|
||||||
|
{
|
||||||
|
return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " "));
|
||||||
|
})).TrimEnd();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从另一个字幕中获取所有Cue,并加载此字幕中,且自动修正偏移
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="webSub"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public WebVttSub AddCuesFromOne(WebVttSub webSub)
|
||||||
|
{
|
||||||
|
FixTimestamp(webSub, this.MpegtsTimestamp);
|
||||||
|
foreach (var item in webSub.Cues)
|
||||||
|
{
|
||||||
|
if (this.Cues.Contains(item)) continue;
|
||||||
|
|
||||||
|
// 如果相差只有1ms,且payload相同,则拼接
|
||||||
|
var last = this.Cues.LastOrDefault();
|
||||||
|
if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload)
|
||||||
|
{
|
||||||
|
last.EndTime = item.EndTime;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.Cues.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FixTimestamp(WebVttSub sub, long baseTimestamp)
|
||||||
|
{
|
||||||
|
if (sub.MpegtsTimestamp == 0)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second
|
||||||
|
var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;
|
||||||
|
var offset = TimeSpan.FromSeconds(seconds);
|
||||||
|
// 当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒,而字幕起始却是2秒),才修复
|
||||||
|
if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset)
|
||||||
|
{
|
||||||
|
foreach (var subCue in sub.Cues)
|
||||||
|
{
|
||||||
|
subCue.StartTime += offset;
|
||||||
|
subCue.EndTime += offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BaseTimestamp != 0)
|
|
||||||
{
|
|
||||||
foreach (var item in webSub.Cues)
|
|
||||||
{
|
|
||||||
if (item.StartTime.TotalMilliseconds - BaseTimestamp >= 0)
|
|
||||||
{
|
|
||||||
item.StartTime = TimeSpan.FromMilliseconds(item.StartTime.TotalMilliseconds - BaseTimestamp);
|
|
||||||
item.EndTime = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - BaseTimestamp);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return webSub;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string RemoveClassTag(string text)
|
|
||||||
{
|
|
||||||
if (VttClassRegex().IsMatch(text))
|
|
||||||
{
|
|
||||||
return string.Join(Environment.NewLine, text.Split('\n').Select(line => line.TrimEnd()).Select(line =>
|
|
||||||
{
|
|
||||||
return string.Concat(VttClassRegex().Matches(line).Select(x => x.Groups[1].Value + " "));
|
|
||||||
})).TrimEnd();
|
|
||||||
}
|
|
||||||
else return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从另一个字幕中获取所有Cue,并加载此字幕中,且自动修正偏移
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="webSub"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public WebVttSub AddCuesFromOne(WebVttSub webSub)
|
|
||||||
{
|
|
||||||
FixTimestamp(webSub, this.MpegtsTimestamp);
|
|
||||||
foreach (var item in webSub.Cues)
|
|
||||||
{
|
|
||||||
if (!this.Cues.Contains(item))
|
|
||||||
{
|
|
||||||
//如果相差只有1ms,且payload相同,则拼接
|
|
||||||
var last = this.Cues.LastOrDefault();
|
|
||||||
if (last != null && this.Cues.Count > 0 && (item.StartTime - last.EndTime).TotalMilliseconds <= 1 && item.Payload == last.Payload)
|
|
||||||
{
|
|
||||||
last.EndTime = item.EndTime;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.Cues.Add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FixTimestamp(WebVttSub sub, long baseTimestamp)
|
|
||||||
{
|
|
||||||
if (sub.MpegtsTimestamp == 0)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
//The MPEG2 transport stream clocks (PCR, PTS, DTS) all have units of 1/90000 second
|
|
||||||
var seconds = (sub.MpegtsTimestamp - baseTimestamp) / 90000;
|
|
||||||
var offset = TimeSpan.FromSeconds(seconds);
|
|
||||||
//当前预添加的字幕的起始时间小于实际上已经走过的时间(如offset已经是100秒,而字幕起始却是2秒),才修复
|
|
||||||
if (sub.Cues.Count > 0 && sub.Cues.First().StartTime < offset)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < sub.Cues.Count; i++)
|
|
||||||
{
|
|
||||||
sub.Cues[i].StartTime += offset;
|
|
||||||
sub.Cues[i].EndTime += offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<SubCue> GetCues()
|
|
||||||
{
|
|
||||||
return this.Cues.Where(c => !string.IsNullOrEmpty(c.Payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TimeSpan ConvertToTS(string str)
|
|
||||||
{
|
|
||||||
//17.0s
|
|
||||||
if (str.EndsWith('s'))
|
|
||||||
{
|
|
||||||
double sec = Convert.ToDouble(str[..^1]);
|
|
||||||
return TimeSpan.FromSeconds(sec);
|
|
||||||
}
|
|
||||||
|
|
||||||
str = str.Replace(',', '.');
|
|
||||||
var ms = Convert.ToInt32(str.Split('.').Last());
|
|
||||||
var o = str.Split('.').First();
|
|
||||||
var t = o.Split(':').Reverse().ToList();
|
|
||||||
var time = 0L + ms;
|
|
||||||
for (int i = 0; i < t.Count(); i++)
|
|
||||||
{
|
|
||||||
time += (long)Math.Pow(60, i) * Convert.ToInt32(t[i]) * 1000;
|
|
||||||
}
|
|
||||||
return TimeSpan.FromMilliseconds(time);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
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.Payload);
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
sb.AppendLine();
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字幕向前平移指定时间
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
public void LeftShiftTime(TimeSpan time)
|
|
||||||
{
|
|
||||||
foreach (var cue in this.Cues)
|
|
||||||
{
|
|
||||||
if (cue.StartTime.TotalSeconds - time.TotalSeconds > 0) cue.StartTime -= time;
|
|
||||||
else cue.StartTime = TimeSpan.FromSeconds(0);
|
|
||||||
|
|
||||||
if (cue.EndTime.TotalSeconds - time.TotalSeconds > 0) cue.EndTime -= time;
|
|
||||||
else cue.EndTime = TimeSpan.FromSeconds(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToVtt()
|
|
||||||
{
|
|
||||||
return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToSrt()
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
int index = 1;
|
|
||||||
foreach (var c in GetCues())
|
|
||||||
{
|
|
||||||
sb.AppendLine($"{index++}");
|
|
||||||
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\,fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\,fff"));
|
|
||||||
sb.AppendLine(c.Payload);
|
|
||||||
sb.AppendLine();
|
|
||||||
}
|
|
||||||
sb.AppendLine();
|
|
||||||
|
|
||||||
var srt = sb.ToString();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(srt.Trim()))
|
|
||||||
{
|
|
||||||
srt = "1\r\n00:00:00,000 --> 00:00:01,000"; //空字幕
|
|
||||||
}
|
|
||||||
|
|
||||||
return srt;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private IEnumerable<SubCue> GetCues()
|
||||||
|
{
|
||||||
|
return this.Cues.Where(c => !string.IsNullOrEmpty(c.Payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan ConvertToTS(string str)
|
||||||
|
{
|
||||||
|
// 17.0s
|
||||||
|
if (str.EndsWith('s'))
|
||||||
|
{
|
||||||
|
double sec = Convert.ToDouble(str[..^1]);
|
||||||
|
return TimeSpan.FromSeconds(sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
str = str.Replace(',', '.');
|
||||||
|
long time = 0;
|
||||||
|
string[] parts = str.Split('.');
|
||||||
|
if (parts.Length > 1)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return TimeSpan.FromMilliseconds(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
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.Payload);
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字幕向前平移指定时间
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="time"></param>
|
||||||
|
public void LeftShiftTime(TimeSpan time)
|
||||||
|
{
|
||||||
|
foreach (var cue in this.Cues)
|
||||||
|
{
|
||||||
|
if (cue.StartTime.TotalSeconds - time.TotalSeconds > 0) cue.StartTime -= time;
|
||||||
|
else cue.StartTime = TimeSpan.FromSeconds(0);
|
||||||
|
|
||||||
|
if (cue.EndTime.TotalSeconds - time.TotalSeconds > 0) cue.EndTime -= time;
|
||||||
|
else cue.EndTime = TimeSpan.FromSeconds(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToVtt()
|
||||||
|
{
|
||||||
|
return "WEBVTT" + Environment.NewLine + Environment.NewLine + ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToSrt()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int index = 1;
|
||||||
|
foreach (var c in GetCues())
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{index++}");
|
||||||
|
sb.AppendLine(c.StartTime.ToString(@"hh\:mm\:ss\,fff") + " --> " + c.EndTime.ToString(@"hh\:mm\:ss\,fff"));
|
||||||
|
sb.AppendLine(c.Payload);
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
sb.AppendLine();
|
||||||
|
|
||||||
|
var srt = sb.ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(srt.Trim()))
|
||||||
|
{
|
||||||
|
srt = "1\r\n00:00:00,000 --> 00:00:01,000"; // 空字幕
|
||||||
|
}
|
||||||
|
|
||||||
|
return srt;
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
{
|
NO = 0
|
||||||
YES = 1,
|
}
|
||||||
NO = 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +1,13 @@
|
|||||||
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,
|
||||||
{
|
AES_128,
|
||||||
NONE,
|
AES_128_ECB,
|
||||||
AES_128,
|
SAMPLE_AES,
|
||||||
AES_128_ECB,
|
SAMPLE_AES_CTR,
|
||||||
SAMPLE_AES,
|
CENC,
|
||||||
SAMPLE_AES_CTR,
|
CHACHA20,
|
||||||
CENC,
|
UNKNOWN
|
||||||
CHACHA20,
|
}
|
||||||
UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
||||||
{
|
HLS,
|
||||||
MPEG_DASH,
|
HTTP_LIVE,
|
||||||
HLS,
|
MSS
|
||||||
HTTP_LIVE,
|
}
|
||||||
MSS
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
||||||
{
|
VIDEO = 1,
|
||||||
AUDIO = 0,
|
SUBTITLES = 2,
|
||||||
VIDEO = 1,
|
CLOSED_CAPTIONS = 3
|
||||||
SUBTITLES = 2,
|
}
|
||||||
CLOSED_CAPTIONS = 3
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +1,15 @@
|
|||||||
namespace N_m3u8DL_RE.Common.Enum
|
namespace N_m3u8DL_RE.Common.Enum;
|
||||||
|
|
||||||
|
public enum RoleType
|
||||||
{
|
{
|
||||||
public enum RoleType
|
Subtitle = 0,
|
||||||
{
|
Main = 1,
|
||||||
Subtitle = 0,
|
Alternate = 2,
|
||||||
Main = 1,
|
Supplementary = 3,
|
||||||
Alternate = 2,
|
Commentary = 4,
|
||||||
Supplementary = 3,
|
Dub = 5,
|
||||||
Commentary = 4,
|
Description = 6,
|
||||||
Dub = 5,
|
Sign = 7,
|
||||||
Description = 6,
|
Metadata = 8,
|
||||||
Sign = 7,
|
ForcedSubtitle = 9
|
||||||
Metadata = 8,
|
}
|
||||||
}
|
|
||||||
}
|
|
@ -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 { }
|
||||||
}
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
|
@ -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>
|
||||||
|
@ -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,
|
||||||
{
|
ERROR,
|
||||||
OFF,
|
WARN,
|
||||||
ERROR,
|
INFO,
|
||||||
WARN,
|
DEBUG,
|
||||||
INFO,
|
}
|
||||||
DEBUG,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,243 +1,226 @@
|
|||||||
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("{}")]
|
||||||
|
private static partial Regex VarsRepRegex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 日志级别,默认为INFO
|
||||||
|
/// </summary>
|
||||||
|
public static LogLevel LogLevel { get; set; } = LogLevel.INFO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否写出日志文件
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsWriteFile { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次运行日志文件所在位置
|
||||||
|
/// </summary>
|
||||||
|
private static string? LogFilePath { get; set; }
|
||||||
|
|
||||||
|
// 读写锁
|
||||||
|
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
|
||||||
|
|
||||||
|
public static void InitLogFile()
|
||||||
{
|
{
|
||||||
[GeneratedRegex("{}")]
|
if (!IsWriteFile) return;
|
||||||
private static partial Regex VarsRepRegex();
|
|
||||||
|
|
||||||
/// <summary>
|
try
|
||||||
/// 日志级别,默认为INFO
|
|
||||||
/// </summary>
|
|
||||||
public static LogLevel LogLevel { get; set; } = LogLevel.INFO;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否写出日志文件
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsWriteFile { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 本次运行日志文件所在位置
|
|
||||||
/// </summary>
|
|
||||||
private static string? LogFilePath { get; set; }
|
|
||||||
|
|
||||||
//读写锁
|
|
||||||
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
|
|
||||||
|
|
||||||
public static void InitLogFile()
|
|
||||||
{
|
{
|
||||||
if (!IsWriteFile) return;
|
var logDir = Path.GetDirectoryName(Environment.ProcessPath) + "/Logs";
|
||||||
|
if (!Directory.Exists(logDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(logDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
LogFilePath = Path.Combine(logDir, now.ToString("yyyy-MM-dd_HH-mm-ss-fff") + ".log");
|
||||||
|
int index = 1;
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(LogFilePath);
|
||||||
|
string init = "LOG " + now.ToString("yyyy/MM/dd") + Environment.NewLine
|
||||||
|
+ "Save Path: " + Path.GetDirectoryName(LogFilePath) + Environment.NewLine
|
||||||
|
+ "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine
|
||||||
|
+ "Task CommandLine: " + Environment.CommandLine;
|
||||||
|
init += $"{Environment.NewLine}{Environment.NewLine}";
|
||||||
|
// 若文件存在则加序号
|
||||||
|
while (File.Exists(LogFilePath))
|
||||||
|
{
|
||||||
|
LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log");
|
||||||
|
}
|
||||||
|
File.WriteAllText(LogFilePath, init, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Error($"Init log failed! {ex.Message.RemoveMarkup()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCurrTime()
|
||||||
|
{
|
||||||
|
return DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleLog(string write, string subWrite = "")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (subWrite == "")
|
||||||
|
{
|
||||||
|
CustomAnsiConsole.MarkupLine(write);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CustomAnsiConsole.Markup(write);
|
||||||
|
Console.WriteLine(subWrite);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsWriteFile || !File.Exists(LogFilePath)) return;
|
||||||
|
|
||||||
|
var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var logDir = Path.GetDirectoryName(Environment.ProcessPath) + "/Logs";
|
// 进入写入
|
||||||
if (!Directory.Exists(logDir))
|
LogWriteLock.EnterWriteLock();
|
||||||
|
using (StreamWriter sw = File.AppendText(LogFilePath))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(logDir);
|
sw.WriteLine(plain);
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTime.Now;
|
|
||||||
LogFilePath = Path.Combine(logDir, now.ToString("yyyy-MM-dd_HH-mm-ss-fff") + ".log");
|
|
||||||
int index = 1;
|
|
||||||
var fileName = Path.GetFileNameWithoutExtension(LogFilePath);
|
|
||||||
string init = "LOG " + now.ToString("yyyy/MM/dd") + Environment.NewLine
|
|
||||||
+ "Save Path: " + Path.GetDirectoryName(LogFilePath) + Environment.NewLine
|
|
||||||
+ "Task Start: " + now.ToString("yyyy/MM/dd HH:mm:ss") + Environment.NewLine
|
|
||||||
+ "Task CommandLine: " + Environment.CommandLine;
|
|
||||||
init += $"{Environment.NewLine}{Environment.NewLine}";
|
|
||||||
//若文件存在则加序号
|
|
||||||
while (File.Exists(LogFilePath))
|
|
||||||
{
|
|
||||||
LogFilePath = Path.Combine(Path.GetDirectoryName(LogFilePath)!, $"{fileName}-{index++}.log");
|
|
||||||
}
|
|
||||||
File.WriteAllText(LogFilePath, init, Encoding.UTF8);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Error($"Init log failed! {ex.Message.RemoveMarkup()}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetCurrTime()
|
|
||||||
{
|
|
||||||
return DateTime.Now.ToString("HH:mm:ss.fff");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void HandleLog(string write, string subWrite = "")
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (subWrite == "")
|
|
||||||
{
|
|
||||||
CustomAnsiConsole.MarkupLine(write);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CustomAnsiConsole.Markup(write);
|
|
||||||
Console.WriteLine(subWrite);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsWriteFile && File.Exists(LogFilePath))
|
|
||||||
{
|
|
||||||
var plain = write.RemoveMarkup() + subWrite.RemoveMarkup();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//进入写入
|
|
||||||
LogWriteLock.EnterWriteLock();
|
|
||||||
using (StreamWriter sw = File.AppendText(LogFilePath))
|
|
||||||
{
|
|
||||||
sw.WriteLine(plain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
//释放占用
|
|
||||||
LogWriteLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
finally
|
||||||
{
|
{
|
||||||
Console.WriteLine("Failed to write: " + write);
|
// 释放占用
|
||||||
|
LogWriteLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
private static string ReplaceVars(string data, params object[] ps)
|
|
||||||
{
|
{
|
||||||
for (int i = 0; i < ps.Length; i++)
|
Console.WriteLine("Failed to write: " + write);
|
||||||
{
|
}
|
||||||
data = VarsRepRegex().Replace(data, $"{ps[i]}", 1);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
private static string ReplaceVars(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ps.Length; i++)
|
||||||
|
{
|
||||||
|
data = VarsRepRegex().Replace(data, $"{ps[i]}", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Info(string data, params object[] ps)
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Info(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.INFO) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
|
||||||
|
HandleLog(write, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void InfoMarkUp(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.INFO) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
|
||||||
|
HandleLog(write);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Debug(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.DEBUG) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
|
||||||
|
HandleLog(write, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DebugMarkUp(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.DEBUG) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
|
||||||
|
HandleLog(write);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Warn(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.WARN) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
|
||||||
|
HandleLog(write, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WarnMarkUp(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.WARN) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
|
||||||
|
HandleLog(write);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.ERROR) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
|
||||||
|
HandleLog(write, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ErrorMarkUp(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (LogLevel < LogLevel.ERROR) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
|
||||||
|
HandleLog(write);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ErrorMarkUp(Exception exception)
|
||||||
|
{
|
||||||
|
string data = exception.Message.EscapeMarkup();
|
||||||
|
if (LogLevel >= LogLevel.ERROR)
|
||||||
{
|
{
|
||||||
if (LogLevel >= LogLevel.INFO)
|
data = exception.ToString().EscapeMarkup();
|
||||||
{
|
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : ";
|
|
||||||
HandleLog(write, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void InfoMarkUp(string data, params object[] ps)
|
ErrorMarkUp(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This thing will only write to the log file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <param name="ps"></param>
|
||||||
|
public static void Extra(string data, params object[] ps)
|
||||||
|
{
|
||||||
|
if (!IsWriteFile || !File.Exists(LogFilePath)) return;
|
||||||
|
|
||||||
|
data = ReplaceVars(data, ps);
|
||||||
|
var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (LogLevel >= LogLevel.INFO)
|
// 进入写入
|
||||||
|
LogWriteLock.EnterWriteLock();
|
||||||
|
using (StreamWriter sw = File.AppendText(LogFilePath))
|
||||||
{
|
{
|
||||||
data = ReplaceVars(data, ps);
|
sw.WriteLine(plain, Encoding.UTF8);
|
||||||
var write = GetCurrTime() + " " + "[underline #548c26]INFO[/] : " + data;
|
|
||||||
HandleLog(write);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
public static void Debug(string data, params object[] ps)
|
|
||||||
{
|
{
|
||||||
if (LogLevel >= LogLevel.DEBUG)
|
// 释放占用
|
||||||
{
|
LogWriteLock.ExitWriteLock();
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: ";
|
|
||||||
HandleLog(write, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DebugMarkUp(string data, params object[] ps)
|
|
||||||
{
|
|
||||||
if (LogLevel >= LogLevel.DEBUG)
|
|
||||||
{
|
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var write = GetCurrTime() + " " + "[underline grey]DEBUG[/]: " + data;
|
|
||||||
HandleLog(write);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Warn(string data, params object[] ps)
|
|
||||||
{
|
|
||||||
if (LogLevel >= LogLevel.WARN)
|
|
||||||
{
|
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : ";
|
|
||||||
HandleLog(write, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WarnMarkUp(string data, params object[] ps)
|
|
||||||
{
|
|
||||||
if (LogLevel >= LogLevel.WARN)
|
|
||||||
{
|
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var write = GetCurrTime() + " " + "[underline #a89022]WARN[/] : " + data;
|
|
||||||
HandleLog(write);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Error(string data, params object[] ps)
|
|
||||||
{
|
|
||||||
if (LogLevel >= LogLevel.ERROR)
|
|
||||||
{
|
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: ";
|
|
||||||
HandleLog(write, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ErrorMarkUp(string data, params object[] ps)
|
|
||||||
{
|
|
||||||
if (LogLevel >= LogLevel.ERROR)
|
|
||||||
{
|
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var write = GetCurrTime() + " " + "[underline red1]ERROR[/]: " + data;
|
|
||||||
HandleLog(write);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ErrorMarkUp(Exception exception)
|
|
||||||
{
|
|
||||||
string data = exception.Message.EscapeMarkup();
|
|
||||||
if (LogLevel >= LogLevel.ERROR)
|
|
||||||
{
|
|
||||||
data = exception.ToString().EscapeMarkup();
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorMarkUp(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This thing will only write to the log file.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <param name="ps"></param>
|
|
||||||
public static void Extra(string data, params object[] ps)
|
|
||||||
{
|
|
||||||
if (IsWriteFile && File.Exists(LogFilePath))
|
|
||||||
{
|
|
||||||
data = ReplaceVars(data, ps);
|
|
||||||
var plain = GetCurrTime() + " " + "EXTRA: " + data.RemoveMarkup();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
//进入写入
|
|
||||||
LogWriteLock.EnterWriteLock();
|
|
||||||
using (StreamWriter sw = File.AppendText(LogFilePath))
|
|
||||||
{
|
|
||||||
sw.WriteLine(plain, Encoding.UTF8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
//释放占用
|
|
||||||
LogWriteLock.ExitWriteLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
<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>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</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>
|
||||||
|
@ -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 static readonly string ReLiveTs = "<RE_LIVE_TS>";
|
||||||
|
public static string singleFileRealtimeDecryptWarn => GetText("singleFileRealtimeDecryptWarn");
|
||||||
|
public static string singleFileSplitWarn => GetText("singleFileSplitWarn");
|
||||||
|
public static string customRangeWarn => GetText("customRangeWarn");
|
||||||
|
public static string customRangeFound => GetText("customRangeFound");
|
||||||
|
public static string customAdKeywordsFound => GetText("customAdKeywordsFound");
|
||||||
|
public static string customRangeInvalid => GetText("customRangeInvalid");
|
||||||
|
public static string consoleRedirected => GetText("consoleRedirected");
|
||||||
|
public static string autoBinaryMerge => GetText("autoBinaryMerge");
|
||||||
|
public static string autoBinaryMerge2 => GetText("autoBinaryMerge2");
|
||||||
|
public static string autoBinaryMerge3 => GetText("autoBinaryMerge3");
|
||||||
|
public static string autoBinaryMerge4 => GetText("autoBinaryMerge4");
|
||||||
|
public static string autoBinaryMerge5 => GetText("autoBinaryMerge5");
|
||||||
|
public static string autoBinaryMerge6 => GetText("autoBinaryMerge6");
|
||||||
|
public static string badM3u8 => GetText("badM3u8");
|
||||||
|
public static string binaryMerge => GetText("binaryMerge");
|
||||||
|
public static string checkingLast => GetText("checkingLast");
|
||||||
|
public static string cmd_appendUrlParams => GetText("cmd_appendUrlParams");
|
||||||
|
public static string cmd_autoSelect => GetText("cmd_autoSelect");
|
||||||
|
public static string cmd_disableUpdateCheck => GetText("cmd_disableUpdateCheck");
|
||||||
|
public static string cmd_binaryMerge => GetText("cmd_binaryMerge");
|
||||||
|
public static string cmd_useFFmpegConcatDemuxer => GetText("cmd_useFFmpegConcatDemuxer");
|
||||||
|
public static string cmd_checkSegmentsCount => GetText("cmd_checkSegmentsCount");
|
||||||
|
public static string cmd_decryptionBinaryPath => GetText("cmd_decryptionBinaryPath");
|
||||||
|
public static string cmd_delAfterDone => GetText("cmd_delAfterDone");
|
||||||
|
public static string cmd_ffmpegBinaryPath => GetText("cmd_ffmpegBinaryPath");
|
||||||
|
public static string cmd_mkvmergeBinaryPath => GetText("cmd_mkvmergeBinaryPath");
|
||||||
|
public static string cmd_baseUrl => GetText("cmd_baseUrl");
|
||||||
|
public static string cmd_maxSpeed => GetText("cmd_maxSpeed");
|
||||||
|
public static string cmd_adKeyword => GetText("cmd_adKeyword");
|
||||||
|
public static string cmd_moreHelp => GetText("cmd_moreHelp");
|
||||||
|
public static string cmd_header => GetText("cmd_header");
|
||||||
|
public static string cmd_muxImport => GetText("cmd_muxImport");
|
||||||
|
public static string cmd_muxImport_more => GetText("cmd_muxImport_more");
|
||||||
|
public static string cmd_selectVideo => GetText("cmd_selectVideo");
|
||||||
|
public static string cmd_dropVideo => GetText("cmd_dropVideo");
|
||||||
|
public static string cmd_selectVideo_more => GetText("cmd_selectVideo_more");
|
||||||
|
public static string cmd_selectAudio => GetText("cmd_selectAudio");
|
||||||
|
public static string cmd_dropAudio => GetText("cmd_dropAudio");
|
||||||
|
public static string cmd_selectAudio_more => GetText("cmd_selectAudio_more");
|
||||||
|
public static string cmd_selectSubtitle => GetText("cmd_selectSubtitle");
|
||||||
|
public static string cmd_dropSubtitle => GetText("cmd_dropSubtitle");
|
||||||
|
public static string cmd_selectSubtitle_more => GetText("cmd_selectSubtitle_more");
|
||||||
|
public static string cmd_custom_range => GetText("cmd_custom_range");
|
||||||
|
public static string cmd_customHLSMethod => GetText("cmd_customHLSMethod");
|
||||||
|
public static string cmd_customHLSKey => GetText("cmd_customHLSKey");
|
||||||
|
public static string cmd_customHLSIv => GetText("cmd_customHLSIv");
|
||||||
|
public static string cmd_Input => GetText("cmd_Input");
|
||||||
|
public static string cmd_forceAnsiConsole => GetText("cmd_forceAnsiConsole");
|
||||||
|
public static string cmd_noAnsiColor => GetText("cmd_noAnsiColor");
|
||||||
|
public static string cmd_keys => GetText("cmd_keys");
|
||||||
|
public static string cmd_keyText => GetText("cmd_keyText");
|
||||||
|
public static string cmd_loadKeyFailed => GetText("cmd_loadKeyFailed");
|
||||||
|
public static string cmd_logLevel => GetText("cmd_logLevel");
|
||||||
|
public static string cmd_MP4RealTimeDecryption => GetText("cmd_MP4RealTimeDecryption");
|
||||||
|
public static string cmd_saveDir => GetText("cmd_saveDir");
|
||||||
|
public static string cmd_saveName => GetText("cmd_saveName");
|
||||||
|
public static string cmd_savePattern => GetText("cmd_savePattern");
|
||||||
|
public static string cmd_skipDownload => GetText("cmd_skipDownload");
|
||||||
|
public static string cmd_noDateInfo => GetText("cmd_noDateInfo");
|
||||||
|
public static string cmd_noLog => GetText("cmd_noLog");
|
||||||
|
public static string cmd_allowHlsMultiExtMap => GetText("cmd_allowHlsMultiExtMap");
|
||||||
|
public static string cmd_skipMerge => GetText("cmd_skipMerge");
|
||||||
|
public static string cmd_subFormat => GetText("cmd_subFormat");
|
||||||
|
public static string cmd_subOnly => GetText("cmd_subOnly");
|
||||||
|
public static string cmd_subtitleFix => GetText("cmd_subtitleFix");
|
||||||
|
public static string cmd_threadCount => GetText("cmd_threadCount");
|
||||||
|
public static string cmd_downloadRetryCount => GetText("cmd_downloadRetryCount");
|
||||||
|
public static string cmd_httpRequestTimeout => GetText("cmd_httpRequestTimeout");
|
||||||
|
public static string cmd_tmpDir => GetText("cmd_tmpDir");
|
||||||
|
public static string cmd_uiLanguage => GetText("cmd_uiLanguage");
|
||||||
|
public static string cmd_urlProcessorArgs => GetText("cmd_urlProcessorArgs");
|
||||||
|
public static string cmd_useShakaPackager => GetText("cmd_useShakaPackager");
|
||||||
|
public static string cmd_decryptionEngine => GetText("cmd_decryptionEngine");
|
||||||
|
public static string cmd_concurrentDownload => GetText("cmd_concurrentDownload");
|
||||||
|
public static string cmd_useSystemProxy => GetText("cmd_useSystemProxy");
|
||||||
|
public static string cmd_customProxy => GetText("cmd_customProxy");
|
||||||
|
public static string cmd_customRange => GetText("cmd_customRange");
|
||||||
|
public static string cmd_liveKeepSegments => GetText("cmd_liveKeepSegments");
|
||||||
|
public static string cmd_livePipeMux => GetText("cmd_livePipeMux");
|
||||||
|
public static string cmd_liveRecordLimit => GetText("cmd_liveRecordLimit");
|
||||||
|
public static string cmd_taskStartAt => GetText("cmd_taskStartAt");
|
||||||
|
public static string cmd_liveWaitTime => GetText("cmd_liveWaitTime");
|
||||||
|
public static string cmd_liveTakeCount => GetText("cmd_liveTakeCount");
|
||||||
|
public static string cmd_liveFixVttByAudio => GetText("cmd_liveFixVttByAudio");
|
||||||
|
public static string cmd_liveRealTimeMerge => GetText("cmd_liveRealTimeMerge");
|
||||||
|
public static string cmd_livePerformAsVod => GetText("cmd_livePerformAsVod");
|
||||||
|
public static string cmd_muxAfterDone => GetText("cmd_muxAfterDone");
|
||||||
|
public static string cmd_muxAfterDone_more => GetText("cmd_muxAfterDone_more");
|
||||||
|
public static string cmd_writeMetaJson => GetText("cmd_writeMetaJson");
|
||||||
|
public static string liveLimit => GetText("liveLimit");
|
||||||
|
public static string realTimeDecMessage => GetText("realTimeDecMessage");
|
||||||
|
public static string liveLimitReached => GetText("liveLimitReached");
|
||||||
|
public static string saveName => GetText("saveName");
|
||||||
|
public static string taskStartAt => GetText("taskStartAt");
|
||||||
|
public static string namedPipeCreated => GetText("namedPipeCreated");
|
||||||
|
public static string namedPipeMux => GetText("namedPipeMux");
|
||||||
|
public static string partMerge => GetText("partMerge");
|
||||||
|
public static string fetch => GetText("fetch");
|
||||||
|
public static string ffmpegMerge => GetText("ffmpegMerge");
|
||||||
|
public static string ffmpegNotFound => GetText("ffmpegNotFound");
|
||||||
|
public static string mkvmergeNotFound => GetText("mkvmergeNotFound");
|
||||||
|
public static string mp4decryptNotFound => GetText("mp4decryptNotFound");
|
||||||
|
public static string shakaPackagerNotFound => GetText("shakaPackagerNotFound");
|
||||||
|
public static string fixingTTML => GetText("fixingTTML");
|
||||||
|
public static string fixingTTMLmp4 => GetText("fixingTTMLmp4");
|
||||||
|
public static string fixingVTT => GetText("fixingVTT");
|
||||||
|
public static string fixingVTTmp4 => GetText("fixingVTTmp4");
|
||||||
|
public static string keyProcessorNotFound => GetText("keyProcessorNotFound");
|
||||||
|
public static string liveFound => GetText("liveFound");
|
||||||
|
public static string loadingUrl => GetText("loadingUrl");
|
||||||
|
public static string masterM3u8Found => GetText("masterM3u8Found");
|
||||||
|
public static string allowHlsMultiExtMap => GetText("allowHlsMultiExtMap");
|
||||||
|
public static string matchDASH => GetText("matchDASH");
|
||||||
|
public static string matchMSS => GetText("matchMSS");
|
||||||
|
public static string matchTS => GetText("matchTS");
|
||||||
|
public static string matchHLS => GetText("matchHLS");
|
||||||
|
public static string notSupported => GetText("notSupported");
|
||||||
|
public static string parsingStream => GetText("parsingStream");
|
||||||
|
public static string promptChoiceText => GetText("promptChoiceText");
|
||||||
|
public static string promptInfo => GetText("promptInfo");
|
||||||
|
public static string promptTitle => GetText("promptTitle");
|
||||||
|
public static string readingInfo => GetText("readingInfo");
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
public readonly static string ReLiveTs = "<RE_LIVE_TS>";
|
if (!StaticText.LANG_DIC.TryGetValue(key, out var textObj))
|
||||||
public static string singleFileRealtimeDecryptWarn { get => GetText("singleFileRealtimeDecryptWarn"); }
|
return "<...LANG TEXT MISSING...>";
|
||||||
public static string singleFileSplitWarn { get => GetText("singleFileSplitWarn"); }
|
|
||||||
public static string customRangeWarn { get => GetText("customRangeWarn"); }
|
|
||||||
public static string customRangeFound { get => GetText("customRangeFound"); }
|
|
||||||
public static string customAdKeywordsFound { get => GetText("customAdKeywordsFound"); }
|
|
||||||
public static string customRangeInvalid { get => GetText("customRangeInvalid"); }
|
|
||||||
public static string consoleRedirected { get => GetText("consoleRedirected"); }
|
|
||||||
public static string autoBinaryMerge { get => GetText("autoBinaryMerge"); }
|
|
||||||
public static string autoBinaryMerge2 { get => GetText("autoBinaryMerge2"); }
|
|
||||||
public static string autoBinaryMerge3 { get => GetText("autoBinaryMerge3"); }
|
|
||||||
public static string autoBinaryMerge4 { get => GetText("autoBinaryMerge4"); }
|
|
||||||
public static string autoBinaryMerge5 { get => GetText("autoBinaryMerge5"); }
|
|
||||||
public static string autoBinaryMerge6 { get => GetText("autoBinaryMerge6"); }
|
|
||||||
public static string badM3u8 { get => GetText("badM3u8"); }
|
|
||||||
public static string binaryMerge { get => GetText("binaryMerge"); }
|
|
||||||
public static string checkingLast { get => GetText("checkingLast"); }
|
|
||||||
public static string cmd_appendUrlParams { get => GetText("cmd_appendUrlParams"); }
|
|
||||||
public static string cmd_autoSelect { get => GetText("cmd_autoSelect"); }
|
|
||||||
public static string cmd_binaryMerge { get => GetText("cmd_binaryMerge"); }
|
|
||||||
public static string cmd_useFFmpegConcatDemuxer { get => GetText("cmd_useFFmpegConcatDemuxer"); }
|
|
||||||
public static string cmd_checkSegmentsCount { get => GetText("cmd_checkSegmentsCount"); }
|
|
||||||
public static string cmd_decryptionBinaryPath { get => GetText("cmd_decryptionBinaryPath"); }
|
|
||||||
public static string cmd_delAfterDone { get => GetText("cmd_delAfterDone"); }
|
|
||||||
public static string cmd_ffmpegBinaryPath { get => GetText("cmd_ffmpegBinaryPath"); }
|
|
||||||
public static string cmd_mkvmergeBinaryPath { get => GetText("cmd_mkvmergeBinaryPath"); }
|
|
||||||
public static string cmd_baseUrl { get => GetText("cmd_baseUrl"); }
|
|
||||||
public static string cmd_maxSpeed { get => GetText("cmd_maxSpeed"); }
|
|
||||||
public static string cmd_adKeyword { get => GetText("cmd_adKeyword"); }
|
|
||||||
public static string cmd_moreHelp { get => GetText("cmd_moreHelp"); }
|
|
||||||
public static string cmd_header { get => GetText("cmd_header"); }
|
|
||||||
public static string cmd_muxImport { get => GetText("cmd_muxImport"); }
|
|
||||||
public static string cmd_muxImport_more { get => GetText("cmd_muxImport_more"); }
|
|
||||||
public static string cmd_selectVideo { get => GetText("cmd_selectVideo"); }
|
|
||||||
public static string cmd_dropVideo { get => GetText("cmd_dropVideo"); }
|
|
||||||
public static string cmd_selectVideo_more { get => GetText("cmd_selectVideo_more"); }
|
|
||||||
public static string cmd_selectAudio { get => GetText("cmd_selectAudio"); }
|
|
||||||
public static string cmd_dropAudio { get => GetText("cmd_dropAudio"); }
|
|
||||||
public static string cmd_selectAudio_more { get => GetText("cmd_selectAudio_more"); }
|
|
||||||
public static string cmd_selectSubtitle { get => GetText("cmd_selectSubtitle"); }
|
|
||||||
public static string cmd_dropSubtitle { get => GetText("cmd_dropSubtitle"); }
|
|
||||||
public static string cmd_selectSubtitle_more { get => GetText("cmd_selectSubtitle_more"); }
|
|
||||||
public static string cmd_custom_range { get => GetText("cmd_custom_range"); }
|
|
||||||
public static string cmd_customHLSMethod { get => GetText("cmd_customHLSMethod"); }
|
|
||||||
public static string cmd_customHLSKey { get => GetText("cmd_customHLSKey"); }
|
|
||||||
public static string cmd_customHLSIv { get => GetText("cmd_customHLSIv"); }
|
|
||||||
public static string cmd_Input { get => GetText("cmd_Input"); }
|
|
||||||
public static string cmd_forceAnsiConsole { get => GetText("cmd_forceAnsiConsole"); }
|
|
||||||
public static string cmd_noAnsiColor { get => GetText("cmd_noAnsiColor"); }
|
|
||||||
public static string cmd_keys { get => GetText("cmd_keys"); }
|
|
||||||
public static string cmd_keyText { get => GetText("cmd_keyText"); }
|
|
||||||
public static string cmd_loadKeyFailed { get => GetText("cmd_loadKeyFailed"); }
|
|
||||||
public static string cmd_logLevel { get => GetText("cmd_logLevel"); }
|
|
||||||
public static string cmd_MP4RealTimeDecryption { get => GetText("cmd_MP4RealTimeDecryption"); }
|
|
||||||
public static string cmd_saveDir { get => GetText("cmd_saveDir"); }
|
|
||||||
public static string cmd_saveName { get => GetText("cmd_saveName"); }
|
|
||||||
public static string cmd_savePattern { get => GetText("cmd_savePattern"); }
|
|
||||||
public static string cmd_skipDownload { get => GetText("cmd_skipDownload"); }
|
|
||||||
public static string cmd_noDateInfo { get => GetText("cmd_noDateInfo"); }
|
|
||||||
public static string cmd_noLog { get => GetText("cmd_noLog"); }
|
|
||||||
public static string cmd_skipMerge { get => GetText("cmd_skipMerge"); }
|
|
||||||
public static string cmd_subFormat { get => GetText("cmd_subFormat"); }
|
|
||||||
public static string cmd_subOnly { get => GetText("cmd_subOnly"); }
|
|
||||||
public static string cmd_subtitleFix { get => GetText("cmd_subtitleFix"); }
|
|
||||||
public static string cmd_threadCount { get => GetText("cmd_threadCount"); }
|
|
||||||
public static string cmd_downloadRetryCount { get => GetText("cmd_downloadRetryCount"); }
|
|
||||||
public static string cmd_tmpDir { get => GetText("cmd_tmpDir"); }
|
|
||||||
public static string cmd_uiLanguage { get => GetText("cmd_uiLanguage"); }
|
|
||||||
public static string cmd_urlProcessorArgs { get => GetText("cmd_urlProcessorArgs"); }
|
|
||||||
public static string cmd_useShakaPackager { get => GetText("cmd_useShakaPackager"); }
|
|
||||||
public static string cmd_concurrentDownload { get => GetText("cmd_concurrentDownload"); }
|
|
||||||
public static string cmd_useSystemProxy { get => GetText("cmd_useSystemProxy"); }
|
|
||||||
public static string cmd_customProxy { get => GetText("cmd_customProxy"); }
|
|
||||||
public static string cmd_customRange { get => GetText("cmd_customRange"); }
|
|
||||||
public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); }
|
|
||||||
public static string cmd_livePipeMux { get => GetText("cmd_livePipeMux"); }
|
|
||||||
public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); }
|
|
||||||
public static string cmd_taskStartAt { get => GetText("cmd_taskStartAt"); }
|
|
||||||
public static string cmd_liveWaitTime { get => GetText("cmd_liveWaitTime"); }
|
|
||||||
public static string cmd_liveTakeCount { get => GetText("cmd_liveTakeCount"); }
|
|
||||||
public static string cmd_liveFixVttByAudio { get => GetText("cmd_liveFixVttByAudio"); }
|
|
||||||
public static string cmd_liveRealTimeMerge { get => GetText("cmd_liveRealTimeMerge"); }
|
|
||||||
public static string cmd_livePerformAsVod { get => GetText("cmd_livePerformAsVod"); }
|
|
||||||
public static string cmd_muxAfterDone { get => GetText("cmd_muxAfterDone"); }
|
|
||||||
public static string cmd_muxAfterDone_more { get => GetText("cmd_muxAfterDone_more"); }
|
|
||||||
public static string cmd_writeMetaJson { get => GetText("cmd_writeMetaJson"); }
|
|
||||||
public static string liveLimit { get => GetText("liveLimit"); }
|
|
||||||
public static string realTimeDecMessage { get => GetText("realTimeDecMessage"); }
|
|
||||||
public static string liveLimitReached { get => GetText("liveLimitReached"); }
|
|
||||||
public static string saveName { get => GetText("saveName"); }
|
|
||||||
public static string taskStartAt { get => GetText("taskStartAt"); }
|
|
||||||
public static string namedPipeCreated { get => GetText("namedPipeCreated"); }
|
|
||||||
public static string namedPipeMux { get => GetText("namedPipeMux"); }
|
|
||||||
public static string partMerge { get => GetText("partMerge"); }
|
|
||||||
public static string fetch { get => GetText("fetch"); }
|
|
||||||
public static string ffmpegMerge { get => GetText("ffmpegMerge"); }
|
|
||||||
public static string ffmpegNotFound { get => GetText("ffmpegNotFound"); }
|
|
||||||
public static string fixingTTML { get => GetText("fixingTTML"); }
|
|
||||||
public static string fixingTTMLmp4 { get => GetText("fixingTTMLmp4"); }
|
|
||||||
public static string fixingVTT { get => GetText("fixingVTT"); }
|
|
||||||
public static string fixingVTTmp4 { get => GetText("fixingVTTmp4"); }
|
|
||||||
public static string keyProcessorNotFound { get => GetText("keyProcessorNotFound"); }
|
|
||||||
public static string liveFound { get => GetText("liveFound"); }
|
|
||||||
public static string loadingUrl { get => GetText("loadingUrl"); }
|
|
||||||
public static string masterM3u8Found { get => GetText("masterM3u8Found"); }
|
|
||||||
public static string matchDASH { get => GetText("matchDASH"); }
|
|
||||||
public static string matchMSS { get => GetText("matchMSS"); }
|
|
||||||
public static string matchTS { get => GetText("matchTS"); }
|
|
||||||
public static string matchHLS { get => GetText("matchHLS"); }
|
|
||||||
public static string notSupported { get => GetText("notSupported"); }
|
|
||||||
public static string parsingStream { get => GetText("parsingStream"); }
|
|
||||||
public static string promptChoiceText { get => GetText("promptChoiceText"); }
|
|
||||||
public static string promptInfo { get => GetText("promptInfo"); }
|
|
||||||
public static string promptTitle { get => GetText("promptTitle"); }
|
|
||||||
public static string readingInfo { get => GetText("readingInfo"); }
|
|
||||||
public static string searchKey { get => GetText("searchKey"); }
|
|
||||||
public static string segmentCountCheckNotPass { get => GetText("segmentCountCheckNotPass"); }
|
|
||||||
public static string selectedStream { get => GetText("selectedStream"); }
|
|
||||||
public static string startDownloading { get => GetText("startDownloading"); }
|
|
||||||
public static string streamsInfo { get => GetText("streamsInfo"); }
|
|
||||||
public static string writeJson { get => GetText("writeJson"); }
|
|
||||||
public static string noStreamsToDownload { get => GetText("noStreamsToDownload"); }
|
|
||||||
public static string newVersionFound { get => GetText("newVersionFound"); }
|
|
||||||
public static string processImageSub { get => GetText("processImageSub"); }
|
|
||||||
|
|
||||||
private static string GetText(string key)
|
if (CurrentLoc is "zh-CN" or "zh-SG" or "zh-Hans")
|
||||||
{
|
return textObj.ZH_CN;
|
||||||
if (!StaticText.LANG_DIC.ContainsKey(key))
|
return CurrentLoc.StartsWith("zh-") ? textObj.ZH_TW : textObj.EN_US;
|
||||||
return "<...LANG TEXT MISSING...>";
|
|
||||||
|
|
||||||
var current = Thread.CurrentThread.CurrentUICulture.Name;
|
|
||||||
if (current == "zh-CN" || current == "zh-SG" || current == "zh-Hans")
|
|
||||||
return StaticText.LANG_DIC[key].ZH_CN;
|
|
||||||
else if (current.StartsWith("zh-"))
|
|
||||||
return StaticText.LANG_DIC[key].ZH_TW;
|
|
||||||
else
|
|
||||||
return StaticText.LANG_DIC[key].EN_US;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,15 @@
|
|||||||
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)
|
||||||
{
|
{
|
||||||
ZH_CN = zhCN;
|
ZH_CN = zhCN;
|
||||||
ZH_TW = zhTW;
|
ZH_TW = zhTW;
|
||||||
EN_US = enUS;
|
EN_US = enUS;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,80 +1,73 @@
|
|||||||
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,
|
||||||
{
|
WriteIndented = true,
|
||||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
WriteIndented = true,
|
Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() }
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
};
|
||||||
Converters = { new JsonStringEnumConverter(), new BytesBase64Converter() }
|
private static readonly JsonContext Context = new JsonContext(Options);
|
||||||
};
|
|
||||||
private static readonly JsonContext Context = new JsonContext(Options);
|
|
||||||
|
|
||||||
public static string ConvertToJson(object o)
|
public static string ConvertToJson(object o)
|
||||||
|
{
|
||||||
|
if (o is StreamSpec s)
|
||||||
{
|
{
|
||||||
if (o is StreamSpec s)
|
return JsonSerializer.Serialize(s, Context.StreamSpec);
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(s, Context.StreamSpec);
|
|
||||||
}
|
|
||||||
else if (o is IOrderedEnumerable<StreamSpec> ss)
|
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);
|
|
||||||
}
|
|
||||||
else if (o is List<StreamSpec> sList)
|
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(sList, Context.ListStreamSpec);
|
|
||||||
}
|
|
||||||
else if (o is IEnumerable<MediaSegment> mList)
|
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment);
|
|
||||||
}
|
|
||||||
return "{NOT SUPPORTED}";
|
|
||||||
}
|
}
|
||||||
|
if (o is IOrderedEnumerable<StreamSpec> ss)
|
||||||
public static string FormatFileSize(double fileSize)
|
|
||||||
{
|
{
|
||||||
return fileSize switch
|
return JsonSerializer.Serialize(ss, Context.IOrderedEnumerableStreamSpec);
|
||||||
{
|
|
||||||
< 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),
|
|
||||||
>= 1024 * 1024 * 1024 => string.Format("{0:########0.00}GB", (double)fileSize / (1024 * 1024 * 1024)),
|
|
||||||
>= 1024 * 1024 => string.Format("{0:####0.00}MB", (double)fileSize / (1024 * 1024)),
|
|
||||||
>= 1024 => string.Format("{0:####0.00}KB", (double)fileSize / 1024),
|
|
||||||
_ => string.Format("{0:####0.00}B", fileSize)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (o is List<StreamSpec> sList)
|
||||||
//此函数用于格式化输出时长
|
|
||||||
public static string FormatTime(int time)
|
|
||||||
{
|
{
|
||||||
TimeSpan ts = new TimeSpan(0, 0, time);
|
return JsonSerializer.Serialize(sList, Context.ListStreamSpec);
|
||||||
string str = "";
|
|
||||||
str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s";
|
|
||||||
return str;
|
|
||||||
}
|
}
|
||||||
|
if (o is IEnumerable<MediaSegment> mList)
|
||||||
/// <summary>
|
|
||||||
/// 寻找可执行程序
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string? FindExecutable(string name)
|
|
||||||
{
|
{
|
||||||
var fileExt = OperatingSystem.IsWindows() ? ".exe" : "";
|
return JsonSerializer.Serialize(mList, Context.IEnumerableMediaSegment);
|
||||||
var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) };
|
|
||||||
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 "{NOT SUPPORTED}";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static string FormatFileSize(double fileSize)
|
||||||
|
{
|
||||||
|
return fileSize switch
|
||||||
|
{
|
||||||
|
< 0 => throw new ArgumentOutOfRangeException(nameof(fileSize)),
|
||||||
|
>= 1024 * 1024 * 1024 => $"{fileSize / (1024 * 1024 * 1024):########0.00}GB",
|
||||||
|
>= 1024 * 1024 => $"{fileSize / (1024 * 1024):####0.00}MB",
|
||||||
|
>= 1024 => $"{fileSize / 1024:####0.00}KB",
|
||||||
|
_ => $"{fileSize:####0.00}B"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 此函数用于格式化输出时长
|
||||||
|
public static string FormatTime(int time)
|
||||||
|
{
|
||||||
|
TimeSpan ts = new TimeSpan(0, 0, time);
|
||||||
|
string str = "";
|
||||||
|
str = (ts.Hours.ToString("00") == "00" ? "" : ts.Hours.ToString("00") + "h") + ts.Minutes.ToString("00") + "m" + ts.Seconds.ToString("00") + "s";
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 寻找可执行程序
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string? FindExecutable(string name)
|
||||||
|
{
|
||||||
|
var fileExt = OperatingSystem.IsWindows() ? ".exe" : "";
|
||||||
|
var searchPath = new[] { Environment.CurrentDirectory, Path.GetDirectoryName(Environment.ProcessPath) };
|
||||||
|
var envPath = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
|
||||||
|
return searchPath.Concat(envPath).Select(p => Path.Combine(p!, name + fileExt)).FirstOrDefault(File.Exists);
|
||||||
|
}
|
||||||
|
}
|
@ -3,139 +3,136 @@ 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,
|
||||||
{
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
AllowAutoRedirect = false,
|
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true,
|
||||||
AutomaticDecompression = DecompressionMethods.All,
|
MaxConnectionsPerServer = 1024,
|
||||||
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true,
|
};
|
||||||
MaxConnectionsPerServer = 1024,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly HttpClient AppHttpClient = new(HttpClientHandler)
|
public static readonly HttpClient AppHttpClient = new(HttpClientHandler)
|
||||||
{
|
{
|
||||||
Timeout = TimeSpan.FromSeconds(100),
|
Timeout = TimeSpan.FromSeconds(100),
|
||||||
DefaultRequestVersion = HttpVersion.Version20,
|
DefaultRequestVersion = HttpVersion.Version20,
|
||||||
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
|
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static async Task<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null)
|
private static async Task<HttpResponseMessage> DoGetAsync(string url, Dictionary<string, string>? headers = null)
|
||||||
|
{
|
||||||
|
Logger.Debug(ResString.fetch + url);
|
||||||
|
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
|
||||||
|
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
|
||||||
|
webRequest.Headers.Connection.Clear();
|
||||||
|
if (headers != null)
|
||||||
{
|
{
|
||||||
Logger.Debug(ResString.fetch + url);
|
foreach (var item in headers)
|
||||||
using var webRequest = new HttpRequestMessage(HttpMethod.Get, url);
|
|
||||||
webRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate");
|
|
||||||
webRequest.Headers.CacheControl = CacheControlHeaderValue.Parse("no-cache");
|
|
||||||
webRequest.Headers.Connection.Clear();
|
|
||||||
if (headers != null)
|
|
||||||
{
|
{
|
||||||
foreach (var item in headers)
|
webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value);
|
||||||
{
|
|
||||||
webRequest.Headers.TryAddWithoutValidation(item.Key, item.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Logger.Debug(webRequest.Headers.ToString());
|
}
|
||||||
//手动处理跳转,以免自定义Headers丢失
|
Logger.Debug(webRequest.Headers.ToString());
|
||||||
var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead);
|
// 手动处理跳转,以免自定义Headers丢失
|
||||||
if (((int)webResponse.StatusCode).ToString().StartsWith("30"))
|
var webResponse = await AppHttpClient.SendAsync(webRequest, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
if (((int)webResponse.StatusCode).ToString().StartsWith("30"))
|
||||||
|
{
|
||||||
|
HttpResponseHeaders respHeaders = webResponse.Headers;
|
||||||
|
Logger.Debug(respHeaders.ToString());
|
||||||
|
if (respHeaders.Location != null)
|
||||||
{
|
{
|
||||||
HttpResponseHeaders respHeaders = webResponse.Headers;
|
var redirectedUrl = "";
|
||||||
Logger.Debug(respHeaders.ToString());
|
if (!respHeaders.Location.IsAbsoluteUri)
|
||||||
if (respHeaders != null && respHeaders.Location != null)
|
|
||||||
{
|
{
|
||||||
var redirectedUrl = "";
|
Uri uri1 = new Uri(url);
|
||||||
if (!respHeaders.Location.IsAbsoluteUri)
|
Uri uri2 = new Uri(uri1, respHeaders.Location);
|
||||||
{
|
redirectedUrl = uri2.ToString();
|
||||||
Uri uri1 = new Uri(url);
|
}
|
||||||
Uri uri2 = new Uri(uri1, respHeaders.Location);
|
else
|
||||||
redirectedUrl = uri2.ToString();
|
{
|
||||||
}
|
redirectedUrl = respHeaders.Location.AbsoluteUri;
|
||||||
else
|
}
|
||||||
{
|
|
||||||
redirectedUrl = respHeaders.Location.AbsoluteUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (redirectedUrl != url)
|
if (redirectedUrl != url)
|
||||||
{
|
{
|
||||||
Logger.Extra($"Redirected => {redirectedUrl}");
|
Logger.Extra($"Redirected => {redirectedUrl}");
|
||||||
return await DoGetAsync(redirectedUrl, headers);
|
return await DoGetAsync(redirectedUrl, headers);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//手动将跳转后的URL设置进去, 用于后续取用
|
|
||||||
webResponse.Headers.Location = new Uri(url);
|
|
||||||
webResponse.EnsureSuccessStatusCode();
|
|
||||||
return webResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<byte[]> GetBytesAsync(string url, Dictionary<string, string>? headers = null)
|
|
||||||
{
|
|
||||||
if (url.StartsWith("file:"))
|
|
||||||
{
|
|
||||||
return await File.ReadAllBytesAsync(new Uri(url).LocalPath);
|
|
||||||
}
|
|
||||||
byte[] bytes = new byte[0];
|
|
||||||
var webResponse = await DoGetAsync(url, headers);
|
|
||||||
bytes = await webResponse.Content.ReadAsByteArrayAsync();
|
|
||||||
Logger.Debug(HexUtil.BytesToHex(bytes, " "));
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取网页源码
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="headers"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
|
|
||||||
{
|
|
||||||
string htmlCode = string.Empty;
|
|
||||||
var webResponse = await DoGetAsync(url, headers);
|
|
||||||
htmlCode = await webResponse.Content.ReadAsStringAsync();
|
|
||||||
Logger.Debug(htmlCode);
|
|
||||||
return htmlCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CheckMPEG2TS(HttpResponseMessage? webResponse)
|
|
||||||
{
|
|
||||||
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower();
|
|
||||||
return mediaType == "video/ts" || mediaType == "video/mp2t" || mediaType == "video/mpeg";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取网页源码和跳转后的URL
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="headers"></param>
|
|
||||||
/// <returns>(Source Code, RedirectedUrl)</returns>
|
|
||||||
public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null)
|
|
||||||
{
|
|
||||||
string htmlCode = string.Empty;
|
|
||||||
var webResponse = await DoGetAsync(url, headers);
|
|
||||||
if (CheckMPEG2TS(webResponse))
|
|
||||||
{
|
|
||||||
htmlCode = ResString.ReLiveTs;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
htmlCode = await webResponse.Content.ReadAsStringAsync();
|
|
||||||
}
|
|
||||||
Logger.Debug(htmlCode);
|
|
||||||
return (htmlCode, webResponse.Headers.Location != null ? webResponse.Headers.Location.AbsoluteUri : url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)
|
|
||||||
{
|
|
||||||
string htmlCode = string.Empty;
|
|
||||||
using HttpRequestMessage request = new(HttpMethod.Post, Url);
|
|
||||||
request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
|
|
||||||
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());
|
|
||||||
request.Content = new ByteArrayContent(postData);
|
|
||||||
var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
|
||||||
htmlCode = await webResponse.Content.ReadAsStringAsync();
|
|
||||||
return htmlCode;
|
|
||||||
}
|
}
|
||||||
|
// 手动将跳转后的URL设置进去, 用于后续取用
|
||||||
|
webResponse.Headers.Location = new Uri(url);
|
||||||
|
webResponse.EnsureSuccessStatusCode();
|
||||||
|
return webResponse;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static async Task<byte[]> GetBytesAsync(string url, Dictionary<string, string>? headers = null)
|
||||||
|
{
|
||||||
|
if (url.StartsWith("file:"))
|
||||||
|
{
|
||||||
|
return await File.ReadAllBytesAsync(new Uri(url).LocalPath);
|
||||||
|
}
|
||||||
|
var webResponse = await DoGetAsync(url, headers);
|
||||||
|
var bytes = await webResponse.Content.ReadAsByteArrayAsync();
|
||||||
|
Logger.Debug(HexUtil.BytesToHex(bytes, " "));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取网页源码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <param name="headers"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<string> GetWebSourceAsync(string url, Dictionary<string, string>? headers = null)
|
||||||
|
{
|
||||||
|
var webResponse = await DoGetAsync(url, headers);
|
||||||
|
string htmlCode = await webResponse.Content.ReadAsStringAsync();
|
||||||
|
Logger.Debug(htmlCode);
|
||||||
|
return htmlCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckMPEG2TS(HttpResponseMessage? webResponse)
|
||||||
|
{
|
||||||
|
var mediaType = webResponse?.Content.Headers.ContentType?.MediaType?.ToLower();
|
||||||
|
return mediaType is "video/ts" or "video/mp2t" or "video/mpeg";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取网页源码和跳转后的URL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <param name="headers"></param>
|
||||||
|
/// <returns>(Source Code, RedirectedUrl)</returns>
|
||||||
|
public static async Task<(string, string)> GetWebSourceAndNewUrlAsync(string url, Dictionary<string, string>? headers = null)
|
||||||
|
{
|
||||||
|
string htmlCode;
|
||||||
|
var webResponse = await DoGetAsync(url, headers);
|
||||||
|
if (CheckMPEG2TS(webResponse))
|
||||||
|
{
|
||||||
|
htmlCode = ResString.ReLiveTs;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
htmlCode = await webResponse.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
Logger.Debug(htmlCode);
|
||||||
|
return (htmlCode, webResponse.Headers.Location != null ? webResponse.Headers.Location.AbsoluteUri : url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetPostResponseAsync(string Url, byte[] postData)
|
||||||
|
{
|
||||||
|
string htmlCode;
|
||||||
|
using HttpRequestMessage request = new(HttpMethod.Post, Url);
|
||||||
|
request.Headers.TryAddWithoutValidation("Content-Type", "application/json");
|
||||||
|
request.Headers.TryAddWithoutValidation("Content-Length", postData.Length.ToString());
|
||||||
|
request.Content = new ByteArrayContent(postData);
|
||||||
|
var webResponse = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
htmlCode = await webResponse.Content.ReadAsStringAsync();
|
||||||
|
return htmlCode;
|
||||||
|
}
|
||||||
|
}
|
@ -1,48 +1,39 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 判断是不是HEX字符串
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool TryParseHexString(string input, out byte[]? bytes)
|
|
||||||
{
|
|
||||||
bytes = null;
|
|
||||||
input = input.ToUpper();
|
|
||||||
if (input.StartsWith("0X"))
|
|
||||||
input = input[2..];
|
|
||||||
if (input.Length % 2 != 0)
|
|
||||||
return false;
|
|
||||||
if (input.Any(c => !"0123456789ABCDEF".Contains(c)))
|
|
||||||
return false;
|
|
||||||
bytes = HexToBytes(input);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] HexToBytes(string hex)
|
|
||||||
{
|
|
||||||
hex = hex.Trim();
|
|
||||||
if (hex.StartsWith("0x") || hex.StartsWith("0X"))
|
|
||||||
hex = hex.Substring(2);
|
|
||||||
byte[] bytes = new byte[hex.Length / 2];
|
|
||||||
|
|
||||||
for (int i = 0; i < hex.Length; i += 2)
|
|
||||||
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是不是HEX字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool TryParseHexString(string input, out byte[]? bytes)
|
||||||
|
{
|
||||||
|
bytes = null;
|
||||||
|
input = input.ToUpper();
|
||||||
|
if (input.StartsWith("0X"))
|
||||||
|
input = input[2..];
|
||||||
|
if (input.Length % 2 != 0)
|
||||||
|
return false;
|
||||||
|
if (input.Any(c => !"0123456789ABCDEF".Contains(c)))
|
||||||
|
return false;
|
||||||
|
bytes = HexToBytes(input);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] HexToBytes(string hex)
|
||||||
|
{
|
||||||
|
var hexSpan = hex.AsSpan().Trim();
|
||||||
|
if (hexSpan.StartsWith("0x") || hexSpan.StartsWith("0X"))
|
||||||
|
{
|
||||||
|
hexSpan = hexSpan[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert.FromHexString(hexSpan);
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -3,66 +3,67 @@ 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> CustomParserArgs { get; } = new();
|
||||||
|
|
||||||
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
public Dictionary<string, string> Headers { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 内容前置处理器. 调用顺序与列表顺序相同
|
/// 内容前置处理器. 调用顺序与列表顺序相同
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<ContentProcessor> ContentProcessors { get; } = new List<ContentProcessor>() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() };
|
public IList<ContentProcessor> ContentProcessors { get; } = new List<ContentProcessor>() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加分片URL前置处理器. 调用顺序与列表顺序相同
|
/// 添加分片URL前置处理器. 调用顺序与列表顺序相同
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<UrlProcessor> UrlProcessors { get; } = new List<UrlProcessor>() { new DefaultUrlProcessor() };
|
public IList<UrlProcessor> UrlProcessors { get; } = new List<UrlProcessor>() { new DefaultUrlProcessor() };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// KEY解析器. 调用顺序与列表顺序相同
|
/// KEY解析器. 调用顺序与列表顺序相同
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<KeyProcessor> KeyProcessors { get; } = new List<KeyProcessor>() { new DefaultHLSKeyProcessor() };
|
public IList<KeyProcessor> KeyProcessors { get; } = new List<KeyProcessor>() { new DefaultHLSKeyProcessor() };
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 自定义的加密方式
|
/// 自定义的加密方式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EncryptMethod? CustomMethod { get; set; }
|
public EncryptMethod? CustomMethod { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 自定义的解密KEY
|
/// 自定义的解密KEY
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? CustomeKey { get; set; }
|
public byte[]? CustomeKey { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 自定义的解密IV
|
/// 自定义的解密IV
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? CustomeIV { get; set; }
|
public byte[]? CustomeIV { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 组装视频分段的URL时,是否要把原本URL后的参数也加上去
|
/// 组装视频分段的URL时,是否要把原本URL后的参数也加上去
|
||||||
/// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx"
|
/// 如 Base URL = "http://xxx.com/playlist.m3u8?hmac=xxx&token=xxx"
|
||||||
/// 相对路径 = clip_01.ts
|
/// 相对路径 = clip_01.ts
|
||||||
/// 如果 AppendUrlParams=false,得 http://xxx.com/clip_01.ts
|
/// 如果 AppendUrlParams=false,得 http://xxx.com/clip_01.ts
|
||||||
/// 如果 AppendUrlParams=true,得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx
|
/// 如果 AppendUrlParams=true,得 http://xxx.com/clip_01.ts?hmac=xxx&token=xxx
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AppendUrlParams { get; set; } = false;
|
public bool AppendUrlParams { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 此参数将会传递给URL Processor中
|
/// 此参数将会传递给URL Processor中
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? UrlProcessorArgs { get; set; }
|
public string? UrlProcessorArgs { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// KEY重试次数
|
/// KEY重试次数
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int KeyRetryCount { get; set; } = 3;
|
public int KeyRetryCount { get; set; } = 3;
|
||||||
}
|
}
|
||||||
}
|
|
@ -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$";
|
|
||||||
}
|
|
||||||
}
|
|
@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
@ -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}";
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,27 +1,21 @@
|
|||||||
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; }
|
||||||
|
|
||||||
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
|
Task<List<StreamSpec>> ExtractStreamsAsync(string rawText);
|
||||||
|
|
||||||
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
|
Task FetchPlayListAsync(List<StreamSpec> streamSpecs);
|
||||||
Task RefreshPlayListAsync(List<StreamSpec> streamSpecs);
|
Task RefreshPlayListAsync(List<StreamSpec> streamSpecs);
|
||||||
|
|
||||||
string PreProcessUrl(string url);
|
string PreProcessUrl(string url);
|
||||||
|
|
||||||
void PreProcessContent();
|
void PreProcessContent();
|
||||||
}
|
}
|
||||||
}
|
|
@ -3,51 +3,50 @@ 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 ParserConfig ParserConfig {get; set;}
|
||||||
|
|
||||||
|
public LiveTSExtractor(ParserConfig parserConfig)
|
||||||
{
|
{
|
||||||
public ExtractorType ExtractorType => ExtractorType.HTTP_LIVE;
|
this.ParserConfig = parserConfig;
|
||||||
|
|
||||||
public ParserConfig ParserConfig {get; set;}
|
|
||||||
|
|
||||||
public LiveTSExtractor(ParserConfig parserConfig)
|
|
||||||
{
|
|
||||||
this.ParserConfig = parserConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
|
||||||
{
|
|
||||||
return new List<StreamSpec>()
|
|
||||||
{
|
|
||||||
new StreamSpec()
|
|
||||||
{
|
|
||||||
OriginalUrl = ParserConfig.OriginalUrl,
|
|
||||||
Url = ParserConfig.Url,
|
|
||||||
Playlist = new Playlist(),
|
|
||||||
GroupId = ResString.ReLiveTs
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void PreProcessContent()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string PreProcessUrl(string url)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new List<StreamSpec>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
OriginalUrl = ParserConfig.OriginalUrl,
|
||||||
|
Url = ParserConfig.Url,
|
||||||
|
Playlist = new Playlist(),
|
||||||
|
GroupId = ResString.ReLiveTs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PreProcessContent()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PreProcessUrl(string url)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
@ -6,396 +6,382 @@ 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
|
[GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")]
|
||||||
//https://test.playready.microsoft.com/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/manifest
|
private static partial Regex VCodecsRegex();
|
||||||
//file:///C:/Users/nilaoda/Downloads/[MS-SSTR]-180316.pdf
|
|
||||||
internal partial class MSSExtractor : IExtractor
|
|
||||||
{
|
|
||||||
[GeneratedRegex("00000001\\d7([0-9a-fA-F]{6})")]
|
|
||||||
private static partial Regex VCodecsRegex();
|
|
||||||
|
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
|
|
||||||
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;
|
private static EncryptMethod DEFAULT_METHOD = EncryptMethod.CENC;
|
||||||
|
|
||||||
public ExtractorType ExtractorType => ExtractorType.MSS;
|
public ExtractorType ExtractorType => ExtractorType.MSS;
|
||||||
|
|
||||||
private string IsmUrl = string.Empty;
|
private string IsmUrl = string.Empty;
|
||||||
private string BaseUrl = string.Empty;
|
private string BaseUrl = string.Empty;
|
||||||
private string IsmContent = string.Empty;
|
private string IsmContent = string.Empty;
|
||||||
public ParserConfig ParserConfig { get; set; }
|
public ParserConfig ParserConfig { get; set; }
|
||||||
|
|
||||||
public MSSExtractor(ParserConfig parserConfig)
|
public MSSExtractor(ParserConfig parserConfig)
|
||||||
|
{
|
||||||
|
this.ParserConfig = parserConfig;
|
||||||
|
SetInitUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetInitUrl()
|
||||||
|
{
|
||||||
|
this.IsmUrl = ParserConfig.Url ?? string.Empty;
|
||||||
|
this.BaseUrl = !string.IsNullOrEmpty(ParserConfig.BaseUrl) ? ParserConfig.BaseUrl : this.IsmUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
||||||
|
{
|
||||||
|
var streamList = new List<StreamSpec>();
|
||||||
|
this.IsmContent = rawText;
|
||||||
|
this.PreProcessContent();
|
||||||
|
|
||||||
|
var xmlDocument = XDocument.Parse(IsmContent);
|
||||||
|
|
||||||
|
// 选中第一个SmoothStreamingMedia节点
|
||||||
|
var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia");
|
||||||
|
var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000";
|
||||||
|
var durationStr = ssmElement.Attribute("Duration")?.Value;
|
||||||
|
var timescale = Convert.ToInt32(timeScaleStr);
|
||||||
|
var isLiveStr = ssmElement.Attribute("IsLive")?.Value;
|
||||||
|
bool isLive = Convert.ToBoolean(isLiveStr ?? "FALSE");
|
||||||
|
|
||||||
|
var isProtection = false;
|
||||||
|
var protectionSystemId = "";
|
||||||
|
var protectionData = "";
|
||||||
|
|
||||||
|
// 加密检测
|
||||||
|
var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Protection");
|
||||||
|
if (protectElement != null)
|
||||||
{
|
{
|
||||||
this.ParserConfig = parserConfig;
|
var protectionHeader = protectElement.Element("ProtectionHeader");
|
||||||
SetInitUrl();
|
if (protectionHeader != null)
|
||||||
}
|
|
||||||
|
|
||||||
private void SetInitUrl()
|
|
||||||
{
|
|
||||||
this.IsmUrl = ParserConfig.Url ?? string.Empty;
|
|
||||||
if (!string.IsNullOrEmpty(ParserConfig.BaseUrl))
|
|
||||||
this.BaseUrl = ParserConfig.BaseUrl;
|
|
||||||
else
|
|
||||||
this.BaseUrl = this.IsmUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<StreamSpec>> ExtractStreamsAsync(string rawText)
|
|
||||||
{
|
|
||||||
var streamList = new List<StreamSpec>();
|
|
||||||
this.IsmContent = rawText;
|
|
||||||
this.PreProcessContent();
|
|
||||||
|
|
||||||
var xmlDocument = XDocument.Parse(IsmContent);
|
|
||||||
|
|
||||||
//选中第一个SmoothStreamingMedia节点
|
|
||||||
var ssmElement = xmlDocument.Elements().First(e => e.Name.LocalName == "SmoothStreamingMedia");
|
|
||||||
var timeScaleStr = ssmElement.Attribute("TimeScale")?.Value ?? "10000000";
|
|
||||||
var durationStr = ssmElement.Attribute("Duration")?.Value;
|
|
||||||
var timescale = Convert.ToInt32(timeScaleStr);
|
|
||||||
var isLiveStr = ssmElement.Attribute("IsLive")?.Value;
|
|
||||||
bool isLive = Convert.ToBoolean(isLiveStr ?? "FALSE");
|
|
||||||
|
|
||||||
var isProtection = false;
|
|
||||||
var protectionSystemId = "";
|
|
||||||
var protectionData = "";
|
|
||||||
|
|
||||||
//加密检测
|
|
||||||
var protectElement = ssmElement.Elements().FirstOrDefault(e => e.Name.LocalName == "Protection");
|
|
||||||
if (protectElement != null)
|
|
||||||
{
|
{
|
||||||
var protectionHeader = protectElement.Element("ProtectionHeader");
|
isProtection = true;
|
||||||
if (protectionHeader != null)
|
protectionSystemId = protectionHeader.Attribute("SystemID")?.Value ?? "9A04F079-9840-4286-AB92-E65BE0885F95";
|
||||||
{
|
protectionData = HexUtil.BytesToHex(Convert.FromBase64String(protectionHeader.Value));
|
||||||
isProtection = true;
|
|
||||||
protectionSystemId = protectionHeader.Attribute("SystemID")?.Value ?? "9A04F079-9840-4286-AB92-E65BE0885F95";
|
|
||||||
protectionData = HexUtil.BytesToHex(Convert.FromBase64String(protectionHeader.Value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//所有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 name = streamIndex.Attribute("Name")?.Value;
|
||||||
|
var subType = streamIndex.Attribute("Subtype")?.Value; // text track
|
||||||
|
// 如果有则不从QualityLevel读取
|
||||||
|
// Bitrate = "{bitrate}" / "{Bitrate}"
|
||||||
|
// StartTimeSubstitution = "{start time}" / "{start_time}"
|
||||||
|
var urlPattern = streamIndex.Attribute("Url")?.Value;
|
||||||
|
var language = streamIndex.Attribute("Language")?.Value;
|
||||||
|
// 去除不规范的语言标签
|
||||||
|
if (language?.Length != 3) language = null;
|
||||||
|
|
||||||
|
// 所有c节点
|
||||||
|
var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == "c");
|
||||||
|
|
||||||
|
// 所有QualityLevel节点
|
||||||
|
var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == "QualityLevel");
|
||||||
|
|
||||||
|
foreach (var qualityLevel in qualityLevelElements)
|
||||||
{
|
{
|
||||||
var type = streamIndex.Attribute("Type")?.Value; //"video" / "audio" / "text"
|
urlPattern = (qualityLevel.Attribute("Url")?.Value ?? urlPattern)!
|
||||||
var name = streamIndex.Attribute("Name")?.Value;
|
.Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime);
|
||||||
var subType = streamIndex.Attribute("Subtype")?.Value; //text track
|
var fourCC = qualityLevel.Attribute("FourCC")!.Value.ToUpper();
|
||||||
//如果有则不从QualityLevel读取
|
var samplingRateStr = qualityLevel.Attribute("SamplingRate")?.Value;
|
||||||
//Bitrate = "{bitrate}" / "{Bitrate}"
|
var bitsPerSampleStr = qualityLevel.Attribute("BitsPerSample")?.Value;
|
||||||
//StartTimeSubstitution = "{start time}" / "{start_time}"
|
var nalUnitLengthFieldStr = qualityLevel.Attribute("NALUnitLengthField")?.Value;
|
||||||
var urlPattern = streamIndex.Attribute("Url")?.Value;
|
var indexStr = qualityLevel.Attribute("Index")?.Value;
|
||||||
var language = streamIndex.Attribute("Language")?.Value;
|
var codecPrivateData = qualityLevel.Attribute("CodecPrivateData")?.Value ?? "";
|
||||||
//去除不规范的语言标签
|
var audioTag = qualityLevel.Attribute("AudioTag")?.Value;
|
||||||
if (language?.Length != 3) language = null;
|
var bitrate = Convert.ToInt32(qualityLevel.Attribute("Bitrate")?.Value ?? "0");
|
||||||
|
var width = Convert.ToInt32(qualityLevel.Attribute("MaxWidth")?.Value ?? "0");
|
||||||
|
var height = Convert.ToInt32(qualityLevel.Attribute("MaxHeight")?.Value ?? "0");
|
||||||
|
var channels = qualityLevel.Attribute("Channels")?.Value;
|
||||||
|
|
||||||
//所有c节点
|
StreamSpec streamSpec = new();
|
||||||
var cElements = streamIndex.Elements().Where(e => e.Name.LocalName == "c");
|
streamSpec.PublishTime = DateTime.Now; // 发布时间默认现在
|
||||||
|
streamSpec.Extension = "m4s";
|
||||||
//所有QualityLevel节点
|
streamSpec.OriginalUrl = ParserConfig.OriginalUrl;
|
||||||
var qualityLevelElements = streamIndex.Elements().Where(e => e.Name.LocalName == "QualityLevel");
|
streamSpec.PeriodId = indexStr;
|
||||||
|
streamSpec.Playlist = new Playlist();
|
||||||
foreach (var qualityLevel in qualityLevelElements)
|
streamSpec.Playlist.IsLive = isLive;
|
||||||
|
streamSpec.Playlist.MediaParts.Add(new MediaPart());
|
||||||
|
streamSpec.GroupId = name ?? indexStr;
|
||||||
|
streamSpec.Bandwidth = bitrate;
|
||||||
|
streamSpec.Codecs = ParseCodecs(fourCC, codecPrivateData);
|
||||||
|
streamSpec.Language = language;
|
||||||
|
streamSpec.Resolution = width == 0 ? null : $"{width}x{height}";
|
||||||
|
streamSpec.Url = IsmUrl;
|
||||||
|
streamSpec.Channels = channels;
|
||||||
|
streamSpec.MediaType = type switch
|
||||||
{
|
{
|
||||||
urlPattern = (qualityLevel.Attribute("Url")?.Value ?? urlPattern)!
|
"text" => MediaType.SUBTITLES,
|
||||||
.Replace(MSSTags.Bitrate_BK, MSSTags.Bitrate).Replace(MSSTags.StartTime_BK, MSSTags.StartTime);
|
"audio" => MediaType.AUDIO,
|
||||||
var fourCC = qualityLevel.Attribute("FourCC")!.Value.ToUpper();
|
_ => null
|
||||||
var samplingRateStr = qualityLevel.Attribute("SamplingRate")?.Value;
|
};
|
||||||
var bitsPerSampleStr = qualityLevel.Attribute("BitsPerSample")?.Value;
|
|
||||||
var nalUnitLengthFieldStr = qualityLevel.Attribute("NALUnitLengthField")?.Value;
|
|
||||||
var indexStr = qualityLevel.Attribute("Index")?.Value;
|
|
||||||
var codecPrivateData = qualityLevel.Attribute("CodecPrivateData")?.Value ?? "";
|
|
||||||
var audioTag = qualityLevel.Attribute("AudioTag")?.Value;
|
|
||||||
var bitrate = Convert.ToInt32(qualityLevel.Attribute("Bitrate")?.Value ?? "0");
|
|
||||||
var width = Convert.ToInt32(qualityLevel.Attribute("MaxWidth")?.Value ?? "0");
|
|
||||||
var height = Convert.ToInt32(qualityLevel.Attribute("MaxHeight")?.Value ?? "0");
|
|
||||||
var channels = qualityLevel.Attribute("Channels")?.Value;
|
|
||||||
|
|
||||||
StreamSpec streamSpec = new();
|
streamSpec.Playlist.MediaInit = new MediaSegment();
|
||||||
streamSpec.PublishTime = DateTime.Now; //发布时间默认现在
|
if (!string.IsNullOrEmpty(codecPrivateData))
|
||||||
streamSpec.Extension = "m4s";
|
{
|
||||||
streamSpec.OriginalUrl = ParserConfig.OriginalUrl;
|
streamSpec.Playlist.MediaInit.Index = -1; // 便于排序
|
||||||
streamSpec.PeriodId = indexStr;
|
streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}";
|
||||||
streamSpec.Playlist = new Playlist();
|
}
|
||||||
streamSpec.Playlist.IsLive = isLive;
|
|
||||||
streamSpec.Playlist.MediaParts.Add(new MediaPart());
|
|
||||||
streamSpec.GroupId = name ?? indexStr;
|
|
||||||
streamSpec.Bandwidth = bitrate;
|
|
||||||
streamSpec.Codecs = ParseCodecs(fourCC, codecPrivateData);
|
|
||||||
streamSpec.Language = language;
|
|
||||||
streamSpec.Resolution = width == 0 ? null : $"{width}x{height}";
|
|
||||||
streamSpec.Url = IsmUrl;
|
|
||||||
streamSpec.Channels = channels;
|
|
||||||
streamSpec.MediaType = type switch
|
|
||||||
{
|
|
||||||
"text" => MediaType.SUBTITLES,
|
|
||||||
"audio" => MediaType.AUDIO,
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
streamSpec.Playlist.MediaInit = new MediaSegment();
|
var currentTime = 0L;
|
||||||
if (!string.IsNullOrEmpty(codecPrivateData))
|
var segIndex = 0;
|
||||||
|
var varDic = new Dictionary<string, object?>();
|
||||||
|
varDic[MSSTags.Bitrate] = bitrate;
|
||||||
|
|
||||||
|
foreach (var c in cElements)
|
||||||
|
{
|
||||||
|
// 每个C元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration)
|
||||||
|
var _startTimeStr = c.Attribute("t")?.Value;
|
||||||
|
var _durationStr = c.Attribute("d")?.Value;
|
||||||
|
var _repeatCountStr = c.Attribute("r")?.Value;
|
||||||
|
|
||||||
|
if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr);
|
||||||
|
var _duration = Convert.ToInt64(_durationStr);
|
||||||
|
var _repeatCount = Convert.ToInt64(_repeatCountStr);
|
||||||
|
if (_repeatCount > 0)
|
||||||
{
|
{
|
||||||
streamSpec.Playlist.MediaInit.Index = -1; //便于排序
|
// This value is one-based. (A value of 2 means two fragments in the contiguous series).
|
||||||
streamSpec.Playlist.MediaInit.Url = $"hex://{codecPrivateData}";
|
_repeatCount -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentTime = 0L;
|
varDic[MSSTags.StartTime] = currentTime;
|
||||||
var segIndex = 0;
|
var oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
|
||||||
var varDic = new Dictionary<string, object?>();
|
var mediaUrl = ParserUtil.ReplaceVars(oriUrl, varDic);
|
||||||
varDic[MSSTags.Bitrate] = bitrate;
|
MediaSegment mediaSegment = new();
|
||||||
|
mediaSegment.Url = mediaUrl;
|
||||||
foreach (var c in cElements)
|
if (oriUrl.Contains(MSSTags.StartTime))
|
||||||
|
mediaSegment.NameFromVar = currentTime.ToString();
|
||||||
|
mediaSegment.Duration = _duration / (double)timescale;
|
||||||
|
mediaSegment.Index = segIndex++;
|
||||||
|
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
|
||||||
|
if (_repeatCount < 0)
|
||||||
|
{
|
||||||
|
// 负数表示一直重复 直到period结束 注意减掉已经加入的1个片段
|
||||||
|
_repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1;
|
||||||
|
}
|
||||||
|
for (long i = 0; i < _repeatCount; i++)
|
||||||
{
|
{
|
||||||
//每个C元素包含三个属性:@t(start time)\@r(repeat count)\@d(duration)
|
|
||||||
var _startTimeStr = c.Attribute("t")?.Value;
|
|
||||||
var _durationStr = c.Attribute("d")?.Value;
|
|
||||||
var _repeatCountStr = c.Attribute("r")?.Value;
|
|
||||||
|
|
||||||
if (_startTimeStr != null) currentTime = Convert.ToInt64(_startTimeStr);
|
|
||||||
var _duration = Convert.ToInt64(_durationStr);
|
|
||||||
var _repeatCount = Convert.ToInt64(_repeatCountStr);
|
|
||||||
if (_repeatCount > 0)
|
|
||||||
{
|
|
||||||
// This value is one-based. (A value of 2 means two fragments in the contiguous series).
|
|
||||||
_repeatCount -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
varDic[MSSTags.StartTime] = currentTime;
|
|
||||||
var oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
|
|
||||||
var mediaUrl = ParserUtil.ReplaceVars(oriUrl, varDic);
|
|
||||||
MediaSegment mediaSegment = new();
|
|
||||||
mediaSegment.Url = mediaUrl;
|
|
||||||
if (oriUrl.Contains(MSSTags.StartTime))
|
|
||||||
mediaSegment.NameFromVar = currentTime.ToString();
|
|
||||||
mediaSegment.Duration = _duration / (double)timescale;
|
|
||||||
mediaSegment.Index = segIndex++;
|
|
||||||
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(mediaSegment);
|
|
||||||
if (_repeatCount < 0)
|
|
||||||
{
|
|
||||||
//负数表示一直重复 直到period结束 注意减掉已经加入的1个片段
|
|
||||||
_repeatCount = (long)Math.Ceiling(Convert.ToInt64(durationStr) / (double)_duration) - 1;
|
|
||||||
}
|
|
||||||
for (long i = 0; i < _repeatCount; i++)
|
|
||||||
{
|
|
||||||
currentTime += _duration;
|
|
||||||
MediaSegment _mediaSegment = new();
|
|
||||||
varDic[MSSTags.StartTime] = currentTime;
|
|
||||||
var _oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
|
|
||||||
var _mediaUrl = ParserUtil.ReplaceVars(_oriUrl, varDic);
|
|
||||||
_mediaSegment.Url = _mediaUrl;
|
|
||||||
_mediaSegment.Index = segIndex++;
|
|
||||||
_mediaSegment.Duration = _duration / (double)timescale;
|
|
||||||
if (_oriUrl.Contains(MSSTags.StartTime))
|
|
||||||
_mediaSegment.NameFromVar = currentTime.ToString();
|
|
||||||
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment);
|
|
||||||
}
|
|
||||||
currentTime += _duration;
|
currentTime += _duration;
|
||||||
|
MediaSegment _mediaSegment = new();
|
||||||
|
varDic[MSSTags.StartTime] = currentTime;
|
||||||
|
var _oriUrl = ParserUtil.CombineURL(this.BaseUrl, urlPattern!);
|
||||||
|
var _mediaUrl = ParserUtil.ReplaceVars(_oriUrl, varDic);
|
||||||
|
_mediaSegment.Url = _mediaUrl;
|
||||||
|
_mediaSegment.Index = segIndex++;
|
||||||
|
_mediaSegment.Duration = _duration / (double)timescale;
|
||||||
|
if (_oriUrl.Contains(MSSTags.StartTime))
|
||||||
|
_mediaSegment.NameFromVar = currentTime.ToString();
|
||||||
|
streamSpec.Playlist.MediaParts[0].MediaSegments.Add(_mediaSegment);
|
||||||
}
|
}
|
||||||
|
currentTime += _duration;
|
||||||
|
}
|
||||||
|
|
||||||
//生成MOOV数据
|
// 生成MOOV数据
|
||||||
if (MSSMoovProcessor.CanHandle(fourCC!))
|
if (MSSMoovProcessor.CanHandle(fourCC!))
|
||||||
|
{
|
||||||
|
streamSpec.MSSData = new MSSData()
|
||||||
{
|
{
|
||||||
streamSpec.MSSData = new MSSData()
|
FourCC = fourCC!,
|
||||||
|
CodecPrivateData = codecPrivateData,
|
||||||
|
Type = type!,
|
||||||
|
Timesacle = Convert.ToInt32(timeScaleStr),
|
||||||
|
Duration = Convert.ToInt64(durationStr),
|
||||||
|
SamplingRate = Convert.ToInt32(samplingRateStr ?? "48000"),
|
||||||
|
Channels = Convert.ToInt32(channels ?? "2"),
|
||||||
|
BitsPerSample = Convert.ToInt32(bitsPerSampleStr ?? "16"),
|
||||||
|
NalUnitLengthField = Convert.ToInt32(nalUnitLengthFieldStr ?? "4"),
|
||||||
|
IsProtection = isProtection,
|
||||||
|
ProtectionData = protectionData,
|
||||||
|
ProtectionSystemID = protectionSystemId,
|
||||||
|
};
|
||||||
|
var processor = new MSSMoovProcessor(streamSpec);
|
||||||
|
var header = processor.GenHeader(); // trackId可能不正确
|
||||||
|
streamSpec.Playlist!.MediaInit!.Url = $"base64://{Convert.ToBase64String(header)}";
|
||||||
|
// 为音视频写入加密信息
|
||||||
|
if (isProtection && type != "text")
|
||||||
|
{
|
||||||
|
if (streamSpec.Playlist.MediaInit != null)
|
||||||
{
|
{
|
||||||
FourCC = fourCC!,
|
streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD;
|
||||||
CodecPrivateData = codecPrivateData,
|
|
||||||
Type = type!,
|
|
||||||
Timesacle = Convert.ToInt32(timeScaleStr),
|
|
||||||
Duration = Convert.ToInt64(durationStr),
|
|
||||||
SamplingRate = Convert.ToInt32(samplingRateStr ?? "48000"),
|
|
||||||
Channels = Convert.ToInt32(channels ?? "2"),
|
|
||||||
BitsPerSample = Convert.ToInt32(bitsPerSampleStr ?? "16"),
|
|
||||||
NalUnitLengthField = Convert.ToInt32(nalUnitLengthFieldStr ?? "4"),
|
|
||||||
IsProtection = isProtection,
|
|
||||||
ProtectionData = protectionData,
|
|
||||||
ProtectionSystemID = protectionSystemId,
|
|
||||||
};
|
|
||||||
var processor = new MSSMoovProcessor(streamSpec);
|
|
||||||
var header = processor.GenHeader(); //trackId可能不正确
|
|
||||||
streamSpec.Playlist!.MediaInit!.Url = $"base64://{Convert.ToBase64String(header)}";
|
|
||||||
//为音视频写入加密信息
|
|
||||||
if (isProtection && type != "text")
|
|
||||||
{
|
|
||||||
if (streamSpec.Playlist.MediaInit != null)
|
|
||||||
{
|
|
||||||
streamSpec.Playlist.MediaInit.EncryptInfo.Method = DEFAULT_METHOD;
|
|
||||||
}
|
|
||||||
foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)
|
|
||||||
{
|
|
||||||
item.EncryptInfo.Method = DEFAULT_METHOD;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
streamList.Add(streamSpec);
|
foreach (var item in streamSpec.Playlist.MediaParts[0].MediaSegments)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.WarnMarkUp($"[green]{fourCC}[/] not supported! Skiped.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//为视频设置默认轨道
|
|
||||||
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO);
|
|
||||||
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES);
|
|
||||||
foreach (var item in streamList)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(item.Resolution))
|
|
||||||
{
|
|
||||||
if (aL.Any())
|
|
||||||
{
|
|
||||||
item.AudioId = aL.First().GroupId;
|
|
||||||
}
|
|
||||||
if (sL.Any())
|
|
||||||
{
|
|
||||||
item.SubtitleId = sL.First().GroupId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return streamList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 解析编码
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fourCC"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private static string? ParseCodecs(string fourCC, string? privateData)
|
|
||||||
{
|
|
||||||
if (fourCC == "TTML") return "stpp";
|
|
||||||
if (string.IsNullOrEmpty(privateData)) return null;
|
|
||||||
|
|
||||||
return fourCC switch
|
|
||||||
{
|
|
||||||
//AVC视频
|
|
||||||
"H264" or "X264" or "DAVC" or "AVC1" => ParseAVCCodecs(privateData),
|
|
||||||
//AAC音频
|
|
||||||
"AAC" or "AACL" or "AACH" or "AACP" => ParseAACCodecs(fourCC, privateData),
|
|
||||||
//默认返回fourCC本身
|
|
||||||
_ => fourCC.ToLower()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ParseAVCCodecs(string privateData)
|
|
||||||
{
|
|
||||||
var result = VCodecsRegex().Match(privateData).Groups[1].Value;
|
|
||||||
return string.IsNullOrEmpty(result) ? "avc1.4D401E" : $"avc1.{result}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ParseAACCodecs(string fourCC, string privateData)
|
|
||||||
{
|
|
||||||
var mpProfile = 2;
|
|
||||||
if (fourCC == "AACH")
|
|
||||||
{
|
|
||||||
mpProfile = 5; // High Efficiency AAC Profile
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(privateData))
|
|
||||||
{
|
|
||||||
mpProfile = (Convert.ToByte(privateData[..2], 16) & 0xF8) >> 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"mp4a.40.{mpProfile}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
|
||||||
{
|
|
||||||
//这里才调用URL预处理器,节省开销
|
|
||||||
await ProcessUrlAsync(streamSpecs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < streamSpecs.Count; i++)
|
|
||||||
{
|
|
||||||
var playlist = streamSpecs[i].Playlist;
|
|
||||||
if (playlist != null)
|
|
||||||
{
|
|
||||||
if (playlist.MediaInit != null)
|
|
||||||
{
|
|
||||||
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
|
|
||||||
}
|
|
||||||
for (int ii = 0; ii < playlist!.MediaParts.Count; ii++)
|
|
||||||
{
|
|
||||||
var part = playlist.MediaParts[ii];
|
|
||||||
for (int iii = 0; iii < part.MediaSegments.Count; iii++)
|
|
||||||
{
|
{
|
||||||
part.MediaSegments[iii].Url = PreProcessUrl(part.MediaSegments[iii].Url);
|
item.EncryptInfo.Method = DEFAULT_METHOD;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
streamList.Add(streamSpec);
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
}
|
|
||||||
|
|
||||||
public string PreProcessUrl(string url)
|
|
||||||
{
|
|
||||||
foreach (var p in ParserConfig.UrlProcessors)
|
|
||||||
{
|
|
||||||
if (p.CanProcess(ExtractorType, url, ParserConfig))
|
|
||||||
{
|
{
|
||||||
url = p.Process(url, ParserConfig);
|
Logger.WarnMarkUp($"[green]{fourCC}[/] not supported! Skiped.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PreProcessContent()
|
// 为视频设置默认轨道
|
||||||
|
var aL = streamList.Where(s => s.MediaType == MediaType.AUDIO).ToList();
|
||||||
|
var sL = streamList.Where(s => s.MediaType == MediaType.SUBTITLES).ToList();
|
||||||
|
foreach (var item in streamList.Where(item => !string.IsNullOrEmpty(item.Resolution)))
|
||||||
{
|
{
|
||||||
foreach (var p in ParserConfig.ContentProcessors)
|
if (aL.Count != 0)
|
||||||
{
|
{
|
||||||
if (p.CanProcess(ExtractorType, IsmContent, ParserConfig))
|
item.AudioId = aL.First().GroupId;
|
||||||
|
}
|
||||||
|
if (sL.Count != 0)
|
||||||
|
{
|
||||||
|
item.SubtitleId = sL.First().GroupId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(streamList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析编码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fourCC"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static string? ParseCodecs(string fourCC, string? privateData)
|
||||||
|
{
|
||||||
|
if (fourCC == "TTML") return "stpp";
|
||||||
|
if (string.IsNullOrEmpty(privateData)) return null;
|
||||||
|
|
||||||
|
return fourCC switch
|
||||||
|
{
|
||||||
|
// AVC视频
|
||||||
|
"H264" or "X264" or "DAVC" or "AVC1" => ParseAVCCodecs(privateData),
|
||||||
|
// AAC音频
|
||||||
|
"AAC" or "AACL" or "AACH" or "AACP" => ParseAACCodecs(fourCC, privateData),
|
||||||
|
// 默认返回fourCC本身
|
||||||
|
_ => fourCC.ToLower()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ParseAVCCodecs(string privateData)
|
||||||
|
{
|
||||||
|
var result = VCodecsRegex().Match(privateData).Groups[1].Value;
|
||||||
|
return string.IsNullOrEmpty(result) ? "avc1.4D401E" : $"avc1.{result}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ParseAACCodecs(string fourCC, string privateData)
|
||||||
|
{
|
||||||
|
var mpProfile = 2;
|
||||||
|
if (fourCC == "AACH")
|
||||||
|
{
|
||||||
|
mpProfile = 5; // High Efficiency AAC Profile
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(privateData))
|
||||||
|
{
|
||||||
|
mpProfile = (Convert.ToByte(privateData[..2], 16) & 0xF8) >> 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"mp4a.40.{mpProfile}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
// 这里才调用URL预处理器,节省开销
|
||||||
|
await ProcessUrlAsync(streamSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ProcessUrlAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
foreach (var streamSpec in streamSpecs)
|
||||||
|
{
|
||||||
|
var playlist = streamSpec.Playlist;
|
||||||
|
if (playlist == null) continue;
|
||||||
|
|
||||||
|
if (playlist.MediaInit != null)
|
||||||
|
{
|
||||||
|
playlist.MediaInit!.Url = PreProcessUrl(playlist.MediaInit!.Url);
|
||||||
|
}
|
||||||
|
for (var ii = 0; ii < playlist!.MediaParts.Count; ii++)
|
||||||
|
{
|
||||||
|
var part = playlist.MediaParts[ii];
|
||||||
|
foreach (var segment in part.MediaSegments)
|
||||||
{
|
{
|
||||||
IsmContent = p.Process(IsmContent, ParserConfig);
|
segment.Url = PreProcessUrl(segment.Url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PreProcessUrl(string url)
|
||||||
|
{
|
||||||
|
foreach (var p in ParserConfig.UrlProcessors)
|
||||||
{
|
{
|
||||||
if (streamSpecs.Count == 0) return;
|
if (p.CanProcess(ExtractorType, url, ParserConfig))
|
||||||
|
|
||||||
var (rawText, url) = ("", ParserConfig.Url);
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.Url, ParserConfig.Headers);
|
url = p.Process(url, ParserConfig);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException) when (ParserConfig.Url != ParserConfig.OriginalUrl)
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PreProcessContent()
|
||||||
|
{
|
||||||
|
foreach (var p in ParserConfig.ContentProcessors)
|
||||||
|
{
|
||||||
|
if (p.CanProcess(ExtractorType, IsmContent, ParserConfig))
|
||||||
{
|
{
|
||||||
//当URL无法访问时,再请求原始URL
|
IsmContent = p.Process(IsmContent, ParserConfig);
|
||||||
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ParserConfig.Url = url;
|
|
||||||
SetInitUrl();
|
|
||||||
|
|
||||||
var newStreams = await ExtractStreamsAsync(rawText);
|
|
||||||
foreach (var streamSpec in streamSpecs)
|
|
||||||
{
|
|
||||||
//有的网站每次请求MPD返回的码率不一致,导致ToShortString()无法匹配 无法更新playlist
|
|
||||||
//故增加通过init url来匹配 (如果有的话)
|
|
||||||
var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());
|
|
||||||
if (!match.Any())
|
|
||||||
match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);
|
|
||||||
|
|
||||||
if (match.Any())
|
|
||||||
streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; //不更新init
|
|
||||||
}
|
|
||||||
//这里才调用URL预处理器,节省开销
|
|
||||||
await ProcessUrlAsync(streamSpecs);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
if (streamSpecs.Count == 0) return;
|
||||||
|
|
||||||
|
var (rawText, url) = ("", ParserConfig.Url);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.Url, ParserConfig.Headers);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException) when (ParserConfig.Url != ParserConfig.OriginalUrl)
|
||||||
|
{
|
||||||
|
// 当URL无法访问时,再请求原始URL
|
||||||
|
(rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(ParserConfig.OriginalUrl, ParserConfig.Headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
ParserConfig.Url = url;
|
||||||
|
SetInitUrl();
|
||||||
|
|
||||||
|
var newStreams = await ExtractStreamsAsync(rawText);
|
||||||
|
foreach (var streamSpec in streamSpecs)
|
||||||
|
{
|
||||||
|
// 有的网站每次请求MPD返回的码率不一致,导致ToShortString()无法匹配 无法更新playlist
|
||||||
|
// 故增加通过init url来匹配 (如果有的话)
|
||||||
|
var match = newStreams.Where(n => n.ToShortString() == streamSpec.ToShortString());
|
||||||
|
if (!match.Any())
|
||||||
|
match = newStreams.Where(n => n.Playlist?.MediaInit?.Url == streamSpec.Playlist?.MediaInit?.Url);
|
||||||
|
|
||||||
|
if (match.Any())
|
||||||
|
streamSpec.Playlist!.MediaParts = match.First().Playlist!.MediaParts; // 不更新init
|
||||||
|
}
|
||||||
|
// 这里才调用URL预处理器,节省开销
|
||||||
|
await ProcessUrlAsync(streamSpecs);
|
||||||
|
}
|
||||||
|
}
|
@ -1,70 +1,62 @@
|
|||||||
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
|
public BinaryReader2(System.IO.Stream stream) : base(stream) { }
|
||||||
class BinaryReader2 : BinaryReader
|
|
||||||
|
public bool HasMoreData()
|
||||||
{
|
{
|
||||||
public BinaryReader2(System.IO.Stream stream) : base(stream) { }
|
return BaseStream.Position < BaseStream.Length;
|
||||||
|
|
||||||
public bool HasMoreData()
|
|
||||||
{
|
|
||||||
return BaseStream.Position < BaseStream.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long GetLength()
|
|
||||||
{
|
|
||||||
return BaseStream.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long GetPosition()
|
|
||||||
{
|
|
||||||
return BaseStream.Position;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int ReadInt32()
|
|
||||||
{
|
|
||||||
var data = base.ReadBytes(4);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
Array.Reverse(data);
|
|
||||||
return BitConverter.ToInt32(data, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override short ReadInt16()
|
|
||||||
{
|
|
||||||
var data = base.ReadBytes(2);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
Array.Reverse(data);
|
|
||||||
return BitConverter.ToInt16(data, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override long ReadInt64()
|
|
||||||
{
|
|
||||||
var data = base.ReadBytes(8);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
Array.Reverse(data);
|
|
||||||
return BitConverter.ToInt64(data, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override uint ReadUInt32()
|
|
||||||
{
|
|
||||||
var data = base.ReadBytes(4);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
Array.Reverse(data);
|
|
||||||
return BitConverter.ToUInt32(data, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ulong ReadUInt64()
|
|
||||||
{
|
|
||||||
var data = base.ReadBytes(8);
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
Array.Reverse(data);
|
|
||||||
return BitConverter.ToUInt64(data, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public long GetLength()
|
||||||
|
{
|
||||||
|
return BaseStream.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long GetPosition()
|
||||||
|
{
|
||||||
|
return BaseStream.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int ReadInt32()
|
||||||
|
{
|
||||||
|
var data = base.ReadBytes(4);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(data);
|
||||||
|
return BitConverter.ToInt32(data, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override short ReadInt16()
|
||||||
|
{
|
||||||
|
var data = base.ReadBytes(2);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(data);
|
||||||
|
return BitConverter.ToInt16(data, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long ReadInt64()
|
||||||
|
{
|
||||||
|
var data = base.ReadBytes(8);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(data);
|
||||||
|
return BitConverter.ToInt64(data, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override uint ReadUInt32()
|
||||||
|
{
|
||||||
|
var data = base.ReadBytes(4);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(data);
|
||||||
|
return BitConverter.ToUInt32(data, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ulong ReadUInt64()
|
||||||
|
{
|
||||||
|
var data = base.ReadBytes(8);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
Array.Reverse(data);
|
||||||
|
return BitConverter.ToUInt64(data, 0);
|
||||||
|
}
|
||||||
|
}
|
@ -1,89 +1,83 @@
|
|||||||
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
|
private static bool IsLittleEndian = BitConverter.IsLittleEndian;
|
||||||
class BinaryWriter2 : BinaryWriter
|
public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
|
||||||
|
|
||||||
|
|
||||||
|
public void WriteUInt(decimal n, int offset = 0)
|
||||||
{
|
{
|
||||||
private static bool IsLittleEndian = BitConverter.IsLittleEndian;
|
var arr = BitConverter.GetBytes((uint)n);
|
||||||
public BinaryWriter2(System.IO.Stream stream) : base(stream) { }
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
if (offset != 0)
|
||||||
public void WriteUInt(decimal n, int offset = 0)
|
arr = arr[offset..];
|
||||||
{
|
BaseStream.Write(arr);
|
||||||
var arr = BitConverter.GetBytes((uint)n);
|
|
||||||
if (IsLittleEndian)
|
|
||||||
Array.Reverse(arr);
|
|
||||||
if (offset != 0)
|
|
||||||
arr = arr[offset..];
|
|
||||||
BaseStream.Write(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(string text)
|
|
||||||
{
|
|
||||||
BaseStream.Write(Encoding.ASCII.GetBytes(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteInt(decimal n, int offset = 0)
|
|
||||||
{
|
|
||||||
var arr = BitConverter.GetBytes((int)n);
|
|
||||||
if (IsLittleEndian)
|
|
||||||
Array.Reverse(arr);
|
|
||||||
if (offset != 0)
|
|
||||||
arr = arr[offset..];
|
|
||||||
BaseStream.Write(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteULong(decimal n, int offset = 0)
|
|
||||||
{
|
|
||||||
var arr = BitConverter.GetBytes((ulong)n);
|
|
||||||
if (IsLittleEndian)
|
|
||||||
Array.Reverse(arr);
|
|
||||||
if (offset != 0)
|
|
||||||
arr = arr[offset..];
|
|
||||||
BaseStream.Write(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteUShort(decimal n, int padding = 0)
|
|
||||||
{
|
|
||||||
var arr = BitConverter.GetBytes((ushort)n);
|
|
||||||
if (IsLittleEndian)
|
|
||||||
Array.Reverse(arr);
|
|
||||||
while (padding > 0)
|
|
||||||
{
|
|
||||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
|
||||||
padding--;
|
|
||||||
}
|
|
||||||
BaseStream.Write(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteShort(decimal n, int padding = 0)
|
|
||||||
{
|
|
||||||
var arr = BitConverter.GetBytes((short)n);
|
|
||||||
if (IsLittleEndian)
|
|
||||||
Array.Reverse(arr);
|
|
||||||
while (padding > 0)
|
|
||||||
{
|
|
||||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
|
||||||
padding--;
|
|
||||||
}
|
|
||||||
BaseStream.Write(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteByte(byte n, int padding = 0)
|
|
||||||
{
|
|
||||||
var arr = new byte[] { n };
|
|
||||||
while (padding > 0)
|
|
||||||
{
|
|
||||||
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
|
||||||
padding--;
|
|
||||||
}
|
|
||||||
BaseStream.Write(arr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override void Write(string text)
|
||||||
|
{
|
||||||
|
BaseStream.Write(Encoding.ASCII.GetBytes(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteInt(decimal n, int offset = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((int)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
if (offset != 0)
|
||||||
|
arr = arr[offset..];
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteULong(decimal n, int offset = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((ulong)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
if (offset != 0)
|
||||||
|
arr = arr[offset..];
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteUShort(decimal n, int padding = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((ushort)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
while (padding > 0)
|
||||||
|
{
|
||||||
|
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||||
|
padding--;
|
||||||
|
}
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteShort(decimal n, int padding = 0)
|
||||||
|
{
|
||||||
|
var arr = BitConverter.GetBytes((short)n);
|
||||||
|
if (IsLittleEndian)
|
||||||
|
Array.Reverse(arr);
|
||||||
|
while (padding > 0)
|
||||||
|
{
|
||||||
|
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||||
|
padding--;
|
||||||
|
}
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteByte(byte n, int padding = 0)
|
||||||
|
{
|
||||||
|
var arr = new byte[] { n };
|
||||||
|
while (padding > 0)
|
||||||
|
{
|
||||||
|
arr = arr.Concat(new byte[] { 0x00 }).ToArray();
|
||||||
|
padding--;
|
||||||
|
}
|
||||||
|
BaseStream.Write(arr);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -3,75 +3,229 @@ 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 required string Region { get; set; }
|
||||||
|
public List<XmlElement> Contents { get; set; } = [];
|
||||||
|
public List<string> ContentStrings { get; set; } = [];
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
public string Begin { get; set; }
|
return obj is SubEntity entity &&
|
||||||
public string End { get; set; }
|
Begin == entity.Begin &&
|
||||||
public string Region { get; set; }
|
End == entity.End &&
|
||||||
public List<XmlElement> Contents { get; set; } = new List<XmlElement>();
|
Region == entity.Region &&
|
||||||
public List<string> ContentStrings { get; set; } = new List<string>();
|
ContentStrings.SequenceEqual(entity.ContentStrings);
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
return obj is SubEntity entity &&
|
|
||||||
Begin == entity.Begin &&
|
|
||||||
End == entity.End &&
|
|
||||||
Region == entity.Region &&
|
|
||||||
ContentStrings.SequenceEqual(entity.ContentStrings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return HashCode.Combine(Begin, End, Region, ContentStrings);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class MP4TtmlUtil
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
[GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")]
|
return HashCode.Combine(Begin, End, Region, ContentStrings);
|
||||||
private static partial Regex AttrRegex();
|
}
|
||||||
[GeneratedRegex("<p.*?>(.+?)<\\/p>")]
|
}
|
||||||
private static partial Regex LabelFixRegex();
|
|
||||||
[GeneratedRegex("\\<tt[\\s\\S]*?\\<\\/tt\\>")]
|
|
||||||
private static partial Regex MultiElementsFixRegex();
|
|
||||||
[GeneratedRegex("\\<smpte:image.*xml:id=\\\"(.*?)\\\".*\\>([\\s\\S]*?)<\\/smpte:image>")]
|
|
||||||
private static partial Regex ImageRegex();
|
|
||||||
|
|
||||||
public static bool CheckInit(byte[] data)
|
public static partial class MP4TtmlUtil
|
||||||
|
{
|
||||||
|
[GeneratedRegex(" \\w+:\\w+=\\\"[^\\\"]*\\\"")]
|
||||||
|
private static partial Regex AttrRegex();
|
||||||
|
[GeneratedRegex("<p.*?>(.+?)<\\/p>")]
|
||||||
|
private static partial Regex LabelFixRegex();
|
||||||
|
[GeneratedRegex(@"\<tt[\s\S]*?\<\/tt\>")]
|
||||||
|
private static partial Regex MultiElementsFixRegex();
|
||||||
|
[GeneratedRegex("\\<smpte:image.*xml:id=\\\"(.*?)\\\".*\\>([\\s\\S]*?)<\\/smpte:image>")]
|
||||||
|
private static partial Regex ImageRegex();
|
||||||
|
|
||||||
|
public static bool CheckInit(byte[] data)
|
||||||
|
{
|
||||||
|
bool sawSTPP = false;
|
||||||
|
|
||||||
|
// parse init
|
||||||
|
new MP4Parser()
|
||||||
|
.Box("moov", MP4Parser.Children)
|
||||||
|
.Box("trak", MP4Parser.Children)
|
||||||
|
.Box("mdia", MP4Parser.Children)
|
||||||
|
.Box("minf", MP4Parser.Children)
|
||||||
|
.Box("stbl", MP4Parser.Children)
|
||||||
|
.FullBox("stsd", MP4Parser.SampleDescription)
|
||||||
|
.Box("stpp", box => {
|
||||||
|
sawSTPP = true;
|
||||||
|
})
|
||||||
|
.Parse(data);
|
||||||
|
|
||||||
|
return sawSTPP;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ShiftTime(string xmlSrc, long segTimeMs, int index)
|
||||||
|
{
|
||||||
|
string Add(string xmlTime)
|
||||||
{
|
{
|
||||||
bool sawSTPP = false;
|
var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
var ts = TimeSpan.FromMilliseconds(dt.TimeOfDay.TotalMilliseconds + segTimeMs * index);
|
||||||
//parse init
|
return $"{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}.{ts.Milliseconds:000}";
|
||||||
new MP4Parser()
|
|
||||||
.Box("moov", MP4Parser.Children)
|
|
||||||
.Box("trak", MP4Parser.Children)
|
|
||||||
.Box("mdia", MP4Parser.Children)
|
|
||||||
.Box("minf", MP4Parser.Children)
|
|
||||||
.Box("stbl", MP4Parser.Children)
|
|
||||||
.FullBox("stsd", MP4Parser.SampleDescription)
|
|
||||||
.Box("stpp", (box) => {
|
|
||||||
sawSTPP = true;
|
|
||||||
})
|
|
||||||
.Parse(data);
|
|
||||||
|
|
||||||
return sawSTPP;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ShiftTime(string xmlSrc, long segTimeMs, int index)
|
if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc;
|
||||||
|
var xmlDoc = new XmlDocument();
|
||||||
|
XmlNamespaceManager? nsMgr = null;
|
||||||
|
xmlDoc.LoadXml(xmlSrc);
|
||||||
|
var ttNode = xmlDoc.LastChild;
|
||||||
|
if (nsMgr == null)
|
||||||
{
|
{
|
||||||
string Add(string xmlTime)
|
var ns = ((XmlElement)ttNode!).GetAttribute("xmlns");
|
||||||
{
|
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
|
||||||
var dt = DateTime.ParseExact(xmlTime, "HH:mm:ss.fff", System.Globalization.CultureInfo.InvariantCulture);
|
nsMgr.AddNamespace("ns", ns);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!xmlSrc.Contains("<tt") || !xmlSrc.Contains("<head>")) return xmlSrc;
|
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
|
||||||
var xmlDoc = new XmlDocument();
|
if (bodyNode == null)
|
||||||
XmlNamespaceManager? nsMgr = null;
|
return xmlSrc;
|
||||||
xmlDoc.LoadXml(xmlSrc);
|
|
||||||
|
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
|
||||||
|
// Parse <p> label
|
||||||
|
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
|
||||||
|
{
|
||||||
|
var _begin = _p.GetAttribute("begin");
|
||||||
|
var _end = _p.GetAttribute("end");
|
||||||
|
// Handle namespace
|
||||||
|
foreach (XmlAttribute attr in _p.Attributes)
|
||||||
|
{
|
||||||
|
if (attr.LocalName == "begin") _begin = attr.Value;
|
||||||
|
else if (attr.LocalName == "end") _end = attr.Value;
|
||||||
|
}
|
||||||
|
_p.SetAttribute("begin", Add(_begin));
|
||||||
|
_p.SetAttribute("end", Add(_end));
|
||||||
|
// Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
|
||||||
|
// Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmlDoc.OuterXml;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTextFromElement(XmlElement node)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (XmlNode item in node.ChildNodes)
|
||||||
|
{
|
||||||
|
if (item.NodeType == XmlNodeType.Text)
|
||||||
|
{
|
||||||
|
sb.Append(item.InnerText.Trim());
|
||||||
|
}
|
||||||
|
else if(item is { NodeType: XmlNodeType.Element, Name: "br" })
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> SplitMultipleRootElements(string xml)
|
||||||
|
{
|
||||||
|
return !MultiElementsFixRegex().IsMatch(xml) ? [] : MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L)
|
||||||
|
{
|
||||||
|
return ExtractFromMp4s([item], segTimeMs, baseTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
||||||
|
{
|
||||||
|
// read ttmls
|
||||||
|
List<string> xmls = [];
|
||||||
|
int segIndex = 0;
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var dataSeg = File.ReadAllBytes(item);
|
||||||
|
|
||||||
|
var sawMDAT = false;
|
||||||
|
// parse media
|
||||||
|
new MP4Parser()
|
||||||
|
.Box("mdat", MP4Parser.AllData(data =>
|
||||||
|
{
|
||||||
|
sawMDAT = true;
|
||||||
|
// Join this to any previous payload, in case the mp4 has multiple
|
||||||
|
// mdats.
|
||||||
|
if (segTimeMs != 0)
|
||||||
|
{
|
||||||
|
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
||||||
|
foreach (var item in datas)
|
||||||
|
{
|
||||||
|
xmls.Add(ShiftTime(item, segTimeMs, segIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
||||||
|
xmls.AddRange(datas);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.Parse(dataSeg,/* partialOkay= */ false);
|
||||||
|
segIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtractSub(xmls, baseTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L)
|
||||||
|
{
|
||||||
|
return ExtractFromTTMLs([item], segTimeMs, baseTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
||||||
|
{
|
||||||
|
// read ttmls
|
||||||
|
List<string> xmls = [];
|
||||||
|
int segIndex = 0;
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var xml = File.ReadAllText(item);
|
||||||
|
xmls.Add(segTimeMs != 0 ? ShiftTime(xml, segTimeMs, segIndex) : xml);
|
||||||
|
segIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtractSub(xmls, baseTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp)
|
||||||
|
{
|
||||||
|
// parsing
|
||||||
|
var xmlDoc = new XmlDocument();
|
||||||
|
var finalSubs = new List<SubEntity>();
|
||||||
|
XmlNode? headNode = null;
|
||||||
|
XmlNamespaceManager? nsMgr = null;
|
||||||
|
var regex = LabelFixRegex();
|
||||||
|
var attrRegex = AttrRegex();
|
||||||
|
foreach (var item in xmls)
|
||||||
|
{
|
||||||
|
var xmlContent = item;
|
||||||
|
if (!xmlContent.Contains("<tt")) continue;
|
||||||
|
|
||||||
|
// fix non-standard xml
|
||||||
|
var xmlContentFix = xmlContent;
|
||||||
|
if (regex.IsMatch(xmlContent))
|
||||||
|
{
|
||||||
|
foreach (Match m in regex.Matches(xmlContentFix))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var inner = m.Groups[1].Value;
|
||||||
|
if (attrRegex.IsMatch(inner))
|
||||||
|
{
|
||||||
|
inner = attrRegex.Replace(inner, "");
|
||||||
|
}
|
||||||
|
new XmlDocument().LoadXml($"<p>{inner}</p>");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
xmlContentFix = xmlContentFix.Replace(m.Groups[1].Value, System.Web.HttpUtility.HtmlEncode(m.Groups[1].Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xmlDoc.LoadXml(xmlContentFix);
|
||||||
var ttNode = xmlDoc.LastChild;
|
var ttNode = xmlDoc.LastChild;
|
||||||
if (nsMgr == null)
|
if (nsMgr == null)
|
||||||
{
|
{
|
||||||
@ -79,309 +233,142 @@ namespace Mp4SubtitleParser
|
|||||||
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
|
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
|
||||||
nsMgr.AddNamespace("ns", ns);
|
nsMgr.AddNamespace("ns", ns);
|
||||||
}
|
}
|
||||||
|
if (headNode == null)
|
||||||
|
headNode = ttNode!.SelectSingleNode("ns:head", nsMgr);
|
||||||
|
|
||||||
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
|
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
|
||||||
if (bodyNode == null)
|
if (bodyNode == null)
|
||||||
return xmlSrc;
|
continue;
|
||||||
|
|
||||||
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
|
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
|
||||||
//Parse <p> label
|
if (_div == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
|
||||||
|
// PNG Subs
|
||||||
|
var imageDic = new Dictionary<string, string>(); // id, Base64
|
||||||
|
if (ImageRegex().IsMatch(xmlDoc.InnerXml))
|
||||||
|
{
|
||||||
|
foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))
|
||||||
|
{
|
||||||
|
imageDic.Add(img.Groups[1].Value.Trim(), img.Groups[2].Value.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert <div> to <p>
|
||||||
|
if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0)
|
||||||
|
{
|
||||||
|
foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!)
|
||||||
|
{
|
||||||
|
var _p = xmlDoc.CreateDocumentFragment();
|
||||||
|
_p.InnerXml = _tDiv.OuterXml.Replace("<div ", "<p ").Replace("</div>", "</p>");
|
||||||
|
_div.AppendChild(_p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
var _region = _p.GetAttribute("region");
|
||||||
|
var _bgImg = _p.GetAttribute("smpte:backgroundImage");
|
||||||
|
// 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;
|
||||||
else if (attr.LocalName == "end") _end = attr.Value;
|
else if (attr.LocalName == "end") _end = attr.Value;
|
||||||
|
else if (attr.LocalName == "region") _region = attr.Value;
|
||||||
}
|
}
|
||||||
_p.SetAttribute("begin", Add(_begin));
|
var sub = new SubEntity
|
||||||
_p.SetAttribute("end", Add(_end));
|
|
||||||
//Console.WriteLine($"{_begin} {_p.GetAttribute("begin")}");
|
|
||||||
//Console.WriteLine($"{_end} {_p.GetAttribute("begin")}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return xmlDoc.OuterXml;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTextFromElement(XmlElement node)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
foreach (XmlNode item in node.ChildNodes)
|
|
||||||
{
|
|
||||||
if (item.NodeType == XmlNodeType.Text)
|
|
||||||
{
|
{
|
||||||
sb.Append(item.InnerText.Trim());
|
Begin = _begin,
|
||||||
}
|
End = _end,
|
||||||
else if(item.NodeType == XmlNodeType.Element && item.Name == "br")
|
Region = _region
|
||||||
|
};
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_bgImg))
|
||||||
{
|
{
|
||||||
sb.AppendLine();
|
var _spans = _p.ChildNodes;
|
||||||
}
|
// Collect <span>
|
||||||
}
|
foreach (XmlNode _node in _spans)
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<string> SplitMultipleRootElements(string xml)
|
|
||||||
{
|
|
||||||
if (!MultiElementsFixRegex().IsMatch(xml)) return new List<string>();
|
|
||||||
return MultiElementsFixRegex().Matches(xml).Select(m => m.Value).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebVttSub ExtractFromMp4(string item, long segTimeMs, long baseTimestamp = 0L)
|
|
||||||
{
|
|
||||||
return ExtractFromMp4s(new string[] { item }, segTimeMs, baseTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebVttSub ExtractFromMp4s(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
|
||||||
{
|
|
||||||
//read ttmls
|
|
||||||
List<string> xmls = new List<string>();
|
|
||||||
int segIndex = 0;
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
var dataSeg = File.ReadAllBytes(item);
|
|
||||||
|
|
||||||
var sawMDAT = false;
|
|
||||||
//parse media
|
|
||||||
new MP4Parser()
|
|
||||||
.Box("mdat", MP4Parser.AllData((data) =>
|
|
||||||
{
|
{
|
||||||
sawMDAT = true;
|
if (_node.NodeType == XmlNodeType.Element)
|
||||||
// Join this to any previous payload, in case the mp4 has multiple
|
|
||||||
// mdats.
|
|
||||||
if (segTimeMs != 0)
|
|
||||||
{
|
{
|
||||||
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
var _span = (XmlElement)_node;
|
||||||
foreach (var item in datas)
|
if (string.IsNullOrEmpty(_span.InnerText))
|
||||||
{
|
continue;
|
||||||
xmls.Add(ShiftTime(item, segTimeMs, segIndex));
|
sub.Contents.Add(_span);
|
||||||
}
|
sub.ContentStrings.Add(_span.OuterXml);
|
||||||
}
|
}
|
||||||
else
|
else if (_node.NodeType == XmlNodeType.Text)
|
||||||
{
|
|
||||||
var datas = SplitMultipleRootElements(Encoding.UTF8.GetString(data));
|
|
||||||
foreach (var item in datas)
|
|
||||||
{
|
|
||||||
xmls.Add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.Parse(dataSeg,/* partialOkay= */ false);
|
|
||||||
segIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExtractSub(xmls, baseTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebVttSub ExtractFromTTML(string item, long segTimeMs, long baseTimestamp = 0L)
|
|
||||||
{
|
|
||||||
return ExtractFromTTMLs(new string[] { item }, segTimeMs, baseTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebVttSub ExtractFromTTMLs(IEnumerable<string> items, long segTimeMs, long baseTimestamp = 0L)
|
|
||||||
{
|
|
||||||
//read ttmls
|
|
||||||
List<string> xmls = new List<string>();
|
|
||||||
int segIndex = 0;
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
var xml = File.ReadAllText(item);
|
|
||||||
if (segTimeMs != 0)
|
|
||||||
{
|
|
||||||
xmls.Add(ShiftTime(xml, segTimeMs, segIndex));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
xmls.Add(xml);
|
|
||||||
}
|
|
||||||
segIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExtractSub(xmls, baseTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static WebVttSub ExtractSub(List<string> xmls, long baseTimestamp)
|
|
||||||
{
|
|
||||||
//parsing
|
|
||||||
var xmlDoc = new XmlDocument();
|
|
||||||
var finalSubs = new List<SubEntity>();
|
|
||||||
XmlNode? headNode = null;
|
|
||||||
XmlNamespaceManager? nsMgr = null;
|
|
||||||
var regex = LabelFixRegex();
|
|
||||||
var attrRegex = AttrRegex();
|
|
||||||
foreach (var item in xmls)
|
|
||||||
{
|
|
||||||
var xmlContent = item;
|
|
||||||
if (!xmlContent.Contains("<tt")) continue;
|
|
||||||
|
|
||||||
//fix non-standard xml
|
|
||||||
var xmlContentFix = xmlContent;
|
|
||||||
if (regex.IsMatch(xmlContent))
|
|
||||||
{
|
|
||||||
foreach (Match m in regex.Matches(xmlContentFix))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var inner = m.Groups[1].Value;
|
|
||||||
if (attrRegex.IsMatch(inner))
|
|
||||||
{
|
|
||||||
inner = attrRegex.Replace(inner, "");
|
|
||||||
}
|
|
||||||
new XmlDocument().LoadXml($"<p>{inner}</p>");
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
xmlContentFix = xmlContentFix.Replace(m.Groups[1].Value, System.Web.HttpUtility.HtmlEncode(m.Groups[1].Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xmlDoc.LoadXml(xmlContentFix);
|
|
||||||
var ttNode = xmlDoc.LastChild;
|
|
||||||
if (nsMgr == null)
|
|
||||||
{
|
|
||||||
var ns = ((XmlElement)ttNode!).GetAttribute("xmlns");
|
|
||||||
nsMgr = new XmlNamespaceManager(xmlDoc.NameTable);
|
|
||||||
nsMgr.AddNamespace("ns", ns);
|
|
||||||
}
|
|
||||||
if (headNode == null)
|
|
||||||
headNode = ttNode!.SelectSingleNode("ns:head", nsMgr);
|
|
||||||
|
|
||||||
var bodyNode = ttNode!.SelectSingleNode("ns:body", nsMgr);
|
|
||||||
if (bodyNode == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var _div = bodyNode.SelectSingleNode("ns:div", nsMgr);
|
|
||||||
if (_div == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
|
|
||||||
//PNG Subs
|
|
||||||
var imageDic = new Dictionary<string, string>(); //id, Base64
|
|
||||||
if (ImageRegex().IsMatch(xmlDoc.InnerXml))
|
|
||||||
{
|
|
||||||
foreach (Match img in ImageRegex().Matches(xmlDoc.InnerXml))
|
|
||||||
{
|
|
||||||
imageDic.Add(img.Groups[1].Value.Trim(), img.Groups[2].Value.Trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//convert <div> to <p>
|
|
||||||
if (_div!.SelectNodes("ns:p", nsMgr) == null || _div!.SelectNodes("ns:p", nsMgr)!.Count == 0)
|
|
||||||
{
|
|
||||||
foreach (XmlElement _tDiv in bodyNode.SelectNodes("ns:div", nsMgr)!)
|
|
||||||
{
|
|
||||||
var _p = xmlDoc.CreateDocumentFragment();
|
|
||||||
_p.InnerXml = _tDiv.OuterXml.Replace("<div ", "<p ").Replace("</div>", "</p>");
|
|
||||||
_div.AppendChild(_p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Parse <p> label
|
|
||||||
foreach (XmlElement _p in _div!.SelectNodes("ns:p", nsMgr)!)
|
|
||||||
{
|
|
||||||
var _begin = _p.GetAttribute("begin");
|
|
||||||
var _end = _p.GetAttribute("end");
|
|
||||||
var _region = _p.GetAttribute("region");
|
|
||||||
var _bgImg = _p.GetAttribute("smpte:backgroundImage");
|
|
||||||
//Handle namespace
|
|
||||||
foreach (XmlAttribute attr in _p.Attributes)
|
|
||||||
{
|
|
||||||
if (attr.LocalName == "begin") _begin = attr.Value;
|
|
||||||
else if (attr.LocalName == "end") _end = attr.Value;
|
|
||||||
else if (attr.LocalName == "region") _region = attr.Value;
|
|
||||||
}
|
|
||||||
var sub = new SubEntity
|
|
||||||
{
|
|
||||||
Begin = _begin,
|
|
||||||
End = _end,
|
|
||||||
Region = _region
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_bgImg))
|
|
||||||
{
|
|
||||||
var _spans = _p.ChildNodes;
|
|
||||||
//Collect <span>
|
|
||||||
foreach (XmlNode _node in _spans)
|
|
||||||
{
|
|
||||||
if (_node.NodeType == XmlNodeType.Element)
|
|
||||||
{
|
|
||||||
var _span = (XmlElement)_node;
|
|
||||||
if (string.IsNullOrEmpty(_span.InnerText))
|
|
||||||
continue;
|
|
||||||
sub.Contents.Add(_span);
|
|
||||||
sub.ContentStrings.Add(_span.OuterXml);
|
|
||||||
}
|
|
||||||
else if (_node.NodeType == XmlNodeType.Text)
|
|
||||||
{
|
|
||||||
var _span = new XmlDocument().CreateElement("span");
|
|
||||||
_span.InnerText = _node.Value!;
|
|
||||||
sub.Contents.Add(_span);
|
|
||||||
sub.ContentStrings.Add(_span.OuterXml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var id = _bgImg.Replace("#", "");
|
|
||||||
if (imageDic.ContainsKey(id))
|
|
||||||
{
|
{
|
||||||
var _span = new XmlDocument().CreateElement("span");
|
var _span = new XmlDocument().CreateElement("span");
|
||||||
_span.InnerText = $"Base64::{imageDic[id]}";
|
_span.InnerText = _node.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
|
|
||||||
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
|
|
||||||
//Skip empty lines
|
|
||||||
if (sub.ContentStrings.Count > 0)
|
|
||||||
{
|
|
||||||
//Extend <p> duration
|
|
||||||
if (index != -1)
|
|
||||||
finalSubs[index].End = sub.End;
|
|
||||||
else if (!finalSubs.Contains(sub))
|
|
||||||
finalSubs.Add(sub);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
|
||||||
|
|
||||||
var dic = new Dictionary<string, string>();
|
|
||||||
foreach (var sub in finalSubs)
|
|
||||||
{
|
|
||||||
var key = $"{sub.Begin} --> {sub.End}";
|
|
||||||
foreach (var item in sub.Contents)
|
|
||||||
{
|
{
|
||||||
if (dic.ContainsKey(key))
|
var id = _bgImg.Replace("#", "");
|
||||||
|
if (imageDic.TryGetValue(id, out var value))
|
||||||
{
|
{
|
||||||
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
var _span = new XmlDocument().CreateElement("span");
|
||||||
dic[key] = $"{dic[key]}\r\n<i>{GetTextFromElement(item)}</i>";
|
_span.InnerText = $"Base64::{value}";
|
||||||
else
|
sub.Contents.Add(_span);
|
||||||
dic[key] = $"{dic[key]}\r\n{GetTextFromElement(item)}";
|
sub.ContentStrings.Add(_span.OuterXml);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
|
||||||
dic.Add(key, $"<i>{GetTextFromElement(item)}</i>");
|
|
||||||
else
|
|
||||||
dic.Add(key, GetTextFromElement(item));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if one <p> has been splitted
|
||||||
|
var index = finalSubs.FindLastIndex(s => s.End == _begin && s.Region == _region && s.ContentStrings.SequenceEqual(sub.ContentStrings));
|
||||||
|
// Skip empty lines
|
||||||
|
if (sub.ContentStrings.Count <= 0)
|
||||||
|
continue;
|
||||||
|
// Extend <p> duration
|
||||||
|
if (index != -1)
|
||||||
|
finalSubs[index].End = sub.End;
|
||||||
|
else if (!finalSubs.Contains(sub))
|
||||||
|
finalSubs.Add(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
StringBuilder vtt = new StringBuilder();
|
|
||||||
vtt.AppendLine("WEBVTT");
|
|
||||||
foreach (var item in dic)
|
|
||||||
{
|
|
||||||
vtt.AppendLine(item.Key);
|
|
||||||
vtt.AppendLine(item.Value);
|
|
||||||
vtt.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
return WebVttSub.Parse(vtt.ToString(), baseTimestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var dic = new Dictionary<string, string>();
|
||||||
|
foreach (var sub in finalSubs)
|
||||||
|
{
|
||||||
|
var key = $"{sub.Begin} --> {sub.End}";
|
||||||
|
foreach (var item in sub.Contents)
|
||||||
|
{
|
||||||
|
if (dic.ContainsKey(key))
|
||||||
|
{
|
||||||
|
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
||||||
|
dic[key] = $"{dic[key]}\r\n<i>{GetTextFromElement(item)}</i>";
|
||||||
|
else
|
||||||
|
dic[key] = $"{dic[key]}\r\n{GetTextFromElement(item)}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (item.GetAttribute("tts:fontStyle") == "italic" || item.GetAttribute("tts:fontStyle") == "oblique")
|
||||||
|
dic.Add(key, $"<i>{GetTextFromElement(item)}</i>");
|
||||||
|
else
|
||||||
|
dic.Add(key, GetTextFromElement(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var vtt = new StringBuilder();
|
||||||
|
vtt.AppendLine("WEBVTT");
|
||||||
|
foreach (var item in dic)
|
||||||
|
{
|
||||||
|
vtt.AppendLine(item.Key);
|
||||||
|
vtt.AppendLine(item.Value);
|
||||||
|
vtt.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebVttSub.Parse(vtt.ToString(), baseTimestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,216 +1,213 @@
|
|||||||
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;
|
||||||
{
|
bool sawWVTT = false;
|
||||||
uint timescale = 0;
|
|
||||||
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))
|
|
||||||
throw new Exception("MDHD version can only be 0 or 1");
|
|
||||||
timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
|
|
||||||
})
|
|
||||||
.Box("minf", MP4Parser.Children)
|
|
||||||
.Box("stbl", MP4Parser.Children)
|
|
||||||
.FullBox("stsd", MP4Parser.SampleDescription)
|
|
||||||
.Box("wvtt", (box) => {
|
|
||||||
// A valid vtt init segment, though we have no actual subtitles yet.
|
|
||||||
sawWVTT = true;
|
|
||||||
})
|
|
||||||
.Parse(data);
|
|
||||||
|
|
||||||
return (sawWVTT, timescale);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebVttSub ExtractSub(IEnumerable<string> files, uint timescale)
|
|
||||||
{
|
|
||||||
if (timescale == 0)
|
|
||||||
throw new Exception("Missing timescale for VTT content!");
|
|
||||||
|
|
||||||
List<SubCue> cues = new();
|
|
||||||
|
|
||||||
foreach (var item in files)
|
|
||||||
{
|
{
|
||||||
var dataSeg = File.ReadAllBytes(item);
|
if (box.Version is not (0 or 1))
|
||||||
|
throw new Exception("MDHD version can only be 0 or 1");
|
||||||
|
timescale = MP4Parser.ParseMDHD(box.Reader, box.Version);
|
||||||
|
})
|
||||||
|
.Box("minf", MP4Parser.Children)
|
||||||
|
.Box("stbl", MP4Parser.Children)
|
||||||
|
.FullBox("stsd", MP4Parser.SampleDescription)
|
||||||
|
.Box("wvtt", _ => {
|
||||||
|
// A valid vtt init segment, though we have no actual subtitles yet.
|
||||||
|
sawWVTT = true;
|
||||||
|
})
|
||||||
|
.Parse(data);
|
||||||
|
|
||||||
bool sawTFDT = false;
|
return (sawWVTT, timescale);
|
||||||
bool sawTRUN = false;
|
}
|
||||||
bool sawMDAT = false;
|
|
||||||
byte[]? rawPayload = null;
|
public static WebVttSub ExtractSub(IEnumerable<string> files, uint timescale)
|
||||||
ulong baseTime = 0;
|
{
|
||||||
ulong defaultDuration = 0;
|
if (timescale == 0)
|
||||||
List<Sample> presentations = new();
|
throw new Exception("Missing timescale for VTT content!");
|
||||||
|
|
||||||
|
List<SubCue> cues = [];
|
||||||
|
|
||||||
|
foreach (var item in files)
|
||||||
|
{
|
||||||
|
var dataSeg = File.ReadAllBytes(item);
|
||||||
|
|
||||||
|
bool sawTFDT = false;
|
||||||
|
bool sawTRUN = false;
|
||||||
|
bool sawMDAT = false;
|
||||||
|
byte[]? rawPayload = null;
|
||||||
|
ulong baseTime = 0;
|
||||||
|
ulong defaultDuration = 0;
|
||||||
|
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;
|
|
||||||
if (!(box.Version == 0 || box.Version == 1))
|
|
||||||
throw new Exception("TFDT version can only be 0 or 1");
|
|
||||||
baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
|
|
||||||
})
|
|
||||||
.FullBox("tfhd", (box) =>
|
|
||||||
{
|
|
||||||
if (box.Flags == 1000)
|
|
||||||
throw new Exception("A TFHD box should have a valid flags value");
|
|
||||||
defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
|
|
||||||
})
|
|
||||||
.FullBox("trun", (box) =>
|
|
||||||
{
|
|
||||||
sawTRUN = true;
|
|
||||||
if (box.Version == 1000)
|
|
||||||
throw new Exception("A TRUN box should have a valid version value");
|
|
||||||
if (box.Flags == 1000)
|
|
||||||
throw new Exception("A TRUN box should have a valid flags value");
|
|
||||||
presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
|
|
||||||
})
|
|
||||||
.Box("mdat", MP4Parser.AllData((data) =>
|
|
||||||
{
|
|
||||||
if (sawMDAT)
|
|
||||||
throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported");
|
|
||||||
sawMDAT = true;
|
|
||||||
rawPayload = data;
|
|
||||||
}))
|
|
||||||
.Parse(dataSeg,/* partialOkay= */ false);
|
|
||||||
|
|
||||||
if (!sawMDAT && !sawTFDT && !sawTRUN)
|
|
||||||
{
|
{
|
||||||
throw new Exception("A required box is missing");
|
sawTFDT = true;
|
||||||
}
|
if (box.Version is not (0 or 1))
|
||||||
|
throw new Exception("TFDT version can only be 0 or 1");
|
||||||
var currentTime = baseTime;
|
baseTime = MP4Parser.ParseTFDT(box.Reader, box.Version);
|
||||||
var reader = new BinaryReader2(new MemoryStream(rawPayload!));
|
})
|
||||||
|
.FullBox("tfhd", box =>
|
||||||
foreach (var presentation in presentations)
|
|
||||||
{
|
{
|
||||||
var duration = presentation.SampleDuration == 0 ? defaultDuration : presentation.SampleDuration;
|
if (box.Flags == 1000)
|
||||||
var startTime = presentation.SampleCompositionTimeOffset != 0 ?
|
throw new Exception("A TFHD box should have a valid flags value");
|
||||||
baseTime + presentation.SampleCompositionTimeOffset :
|
defaultDuration = MP4Parser.ParseTFHD(box.Reader, box.Flags).DefaultSampleDuration;
|
||||||
currentTime;
|
})
|
||||||
currentTime = startTime + duration;
|
.FullBox("trun", box =>
|
||||||
var totalSize = 0;
|
{
|
||||||
do
|
sawTRUN = true;
|
||||||
|
if (box.Version == 1000)
|
||||||
|
throw new Exception("A TRUN box should have a valid version value");
|
||||||
|
if (box.Flags == 1000)
|
||||||
|
throw new Exception("A TRUN box should have a valid flags value");
|
||||||
|
presentations = MP4Parser.ParseTRUN(box.Reader, box.Version, box.Flags).SampleData;
|
||||||
|
})
|
||||||
|
.Box("mdat", MP4Parser.AllData(data =>
|
||||||
|
{
|
||||||
|
if (sawMDAT)
|
||||||
|
throw new Exception("VTT cues in mp4 with multiple MDAT are not currently supported");
|
||||||
|
sawMDAT = true;
|
||||||
|
rawPayload = data;
|
||||||
|
}))
|
||||||
|
.Parse(dataSeg,/* partialOkay= */ false);
|
||||||
|
|
||||||
|
if (!sawMDAT && !sawTFDT && !sawTRUN)
|
||||||
|
{
|
||||||
|
throw new Exception("A required box is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentTime = baseTime;
|
||||||
|
var reader = new BinaryReader2(new MemoryStream(rawPayload!));
|
||||||
|
|
||||||
|
foreach (var presentation in presentations)
|
||||||
|
{
|
||||||
|
var duration = presentation.SampleDuration == 0 ? defaultDuration : presentation.SampleDuration;
|
||||||
|
var startTime = presentation.SampleCompositionTimeOffset != 0 ?
|
||||||
|
baseTime + presentation.SampleCompositionTimeOffset :
|
||||||
|
currentTime;
|
||||||
|
currentTime = startTime + duration;
|
||||||
|
var totalSize = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Read the payload size.
|
||||||
|
var payloadSize = (int)reader.ReadUInt32();
|
||||||
|
totalSize += payloadSize;
|
||||||
|
|
||||||
|
// Skip the type.
|
||||||
|
var payloadType = reader.ReadUInt32();
|
||||||
|
var payloadName = MP4Parser.TypeToString(payloadType);
|
||||||
|
|
||||||
|
// Read the data payload.
|
||||||
|
byte[]? payload = null;
|
||||||
|
if (payloadName == "vttc")
|
||||||
{
|
{
|
||||||
// Read the payload size.
|
if (payloadSize > 8)
|
||||||
var payloadSize = (int)reader.ReadUInt32();
|
|
||||||
totalSize += payloadSize;
|
|
||||||
|
|
||||||
// Skip the type.
|
|
||||||
var payloadType = reader.ReadUInt32();
|
|
||||||
var payloadName = MP4Parser.TypeToString(payloadType);
|
|
||||||
|
|
||||||
// Read the data payload.
|
|
||||||
byte[]? payload = null;
|
|
||||||
if (payloadName == "vttc")
|
|
||||||
{
|
{
|
||||||
if (payloadSize > 8)
|
payload = reader.ReadBytes(payloadSize - 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (payloadName == "vtte")
|
||||||
|
{
|
||||||
|
// It's a vtte, which is a vtt cue that is empty. Ignore any data that
|
||||||
|
// does exist.
|
||||||
|
reader.ReadBytes(payloadSize - 8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Unknown box {payloadName}! Skipping!");
|
||||||
|
reader.ReadBytes(payloadSize - 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration != 0)
|
||||||
|
{
|
||||||
|
if (payload != null)
|
||||||
|
{
|
||||||
|
var cue = ParseVTTC(
|
||||||
|
payload,
|
||||||
|
0 + (double)startTime / timescale,
|
||||||
|
0 + (double)currentTime / timescale);
|
||||||
|
// Check if same subtitle has been splitted
|
||||||
|
if (cue != null)
|
||||||
{
|
{
|
||||||
payload = reader.ReadBytes(payloadSize - 8);
|
var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload);
|
||||||
}
|
if (index != -1)
|
||||||
}
|
|
||||||
else if (payloadName == "vtte")
|
|
||||||
{
|
|
||||||
// It's a vtte, which is a vtt cue that is empty. Ignore any data that
|
|
||||||
// does exist.
|
|
||||||
reader.ReadBytes(payloadSize - 8);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Unknown box {payloadName}! Skipping!");
|
|
||||||
reader.ReadBytes(payloadSize - 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration != 0)
|
|
||||||
{
|
|
||||||
if (payload != null)
|
|
||||||
{
|
|
||||||
if (timescale == 0)
|
|
||||||
throw new Exception("Timescale should not be zero!");
|
|
||||||
var cue = ParseVTTC(
|
|
||||||
payload,
|
|
||||||
0 + (double)startTime / timescale,
|
|
||||||
0 + (double)currentTime / timescale);
|
|
||||||
//Check if same subtitle has been splitted
|
|
||||||
if (cue != null)
|
|
||||||
{
|
{
|
||||||
var index = cues.FindLastIndex(s => s.EndTime == cue.StartTime && s.Settings == cue.Settings && s.Payload == cue.Payload);
|
cues[index].EndTime = cue.EndTime;
|
||||||
if (index != -1)
|
}
|
||||||
{
|
else
|
||||||
cues[index].EndTime = cue.EndTime;
|
{
|
||||||
}
|
cues.Add(cue);
|
||||||
else
|
|
||||||
{
|
|
||||||
cues.Add(cue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Exception("WVTT sample duration unknown, and no default found!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(presentation.SampleSize == 0 || totalSize <= presentation.SampleSize))
|
|
||||||
{
|
|
||||||
throw new Exception("The samples do not fit evenly into the sample sizes given in the TRUN box!");
|
|
||||||
}
|
|
||||||
|
|
||||||
} while (presentation.SampleSize != 0 && (totalSize < presentation.SampleSize));
|
|
||||||
|
|
||||||
if (reader.HasMoreData())
|
|
||||||
{
|
|
||||||
//throw new Exception("MDAT which contain VTT cues and non-VTT data are not currently supported!");
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("WVTT sample duration unknown, and no default found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(presentation.SampleSize == 0 || totalSize <= presentation.SampleSize))
|
||||||
|
{
|
||||||
|
throw new Exception("The samples do not fit evenly into the sample sizes given in the TRUN box!");
|
||||||
|
}
|
||||||
|
|
||||||
|
} while (presentation.SampleSize != 0 && (totalSize < presentation.SampleSize));
|
||||||
|
|
||||||
|
if (reader.HasMoreData())
|
||||||
|
{
|
||||||
|
// throw new Exception("MDAT which contain VTT cues and non-VTT data are not currently supported!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cues.Count > 0)
|
|
||||||
{
|
|
||||||
return new WebVttSub() { Cues = cues };
|
|
||||||
}
|
|
||||||
return new WebVttSub();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SubCue? ParseVTTC(byte[] data, double startTime, double endTime)
|
if (cues.Count > 0)
|
||||||
{
|
{
|
||||||
string payload = string.Empty;
|
return new WebVttSub() { Cues = cues };
|
||||||
string id = string.Empty;
|
|
||||||
string settings = string.Empty;
|
|
||||||
new MP4Parser()
|
|
||||||
.Box("payl", MP4Parser.AllData((data) =>
|
|
||||||
{
|
|
||||||
payload = Encoding.UTF8.GetString(data);
|
|
||||||
}))
|
|
||||||
.Box("iden", MP4Parser.AllData((data) =>
|
|
||||||
{
|
|
||||||
id = Encoding.UTF8.GetString(data);
|
|
||||||
}))
|
|
||||||
.Box("sttg", MP4Parser.AllData((data) =>
|
|
||||||
{
|
|
||||||
settings = Encoding.UTF8.GetString(data);
|
|
||||||
}))
|
|
||||||
.Parse(data);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(payload))
|
|
||||||
{
|
|
||||||
return new SubCue() { StartTime = TimeSpan.FromSeconds(startTime), EndTime = TimeSpan.FromSeconds(endTime), Payload = payload, Settings = settings };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return new WebVttSub();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static SubCue? ParseVTTC(byte[] data, double startTime, double endTime)
|
||||||
|
{
|
||||||
|
string payload = string.Empty;
|
||||||
|
string id = string.Empty;
|
||||||
|
string settings = string.Empty;
|
||||||
|
new MP4Parser()
|
||||||
|
.Box("payl", MP4Parser.AllData(data =>
|
||||||
|
{
|
||||||
|
payload = Encoding.UTF8.GetString(data);
|
||||||
|
}))
|
||||||
|
.Box("iden", MP4Parser.AllData(data =>
|
||||||
|
{
|
||||||
|
id = Encoding.UTF8.GetString(data);
|
||||||
|
}))
|
||||||
|
.Box("sttg", MP4Parser.AllData(data =>
|
||||||
|
{
|
||||||
|
settings = Encoding.UTF8.GetString(data);
|
||||||
|
}))
|
||||||
|
.Parse(data);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(payload))
|
||||||
|
{
|
||||||
|
return new SubCue() { StartTime = TimeSpan.FromSeconds(startTime), EndTime = TimeSpan.FromSeconds(endTime), Payload = payload, Settings = settings };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<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>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -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 string Process(string rawText, ParserConfig parserConfig);
|
||||||
public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig);
|
}
|
||||||
public abstract string Process(string rawText, ParserConfig parserConfig);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +1,26 @@
|
|||||||
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>
|
public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig)
|
||||||
/// 西瓜视频处理
|
|
||||||
/// </summary>
|
|
||||||
public class DefaultDASHContentProcessor : ContentProcessor
|
|
||||||
{
|
{
|
||||||
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)
|
|
||||||
{
|
|
||||||
Logger.Debug("Fix xigua mpd...");
|
|
||||||
mpdContent = mpdContent.Replace("<MPD ", "<MPD xmlns:mas=\"urn:marlin:mas:1-0:services:schemas:mpd\" ");
|
|
||||||
|
|
||||||
return mpdContent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override string Process(string mpdContent, ParserConfig parserConfig)
|
||||||
|
{
|
||||||
|
Logger.Debug("Fix xigua mpd...");
|
||||||
|
mpdContent = mpdContent.Replace("<MPD ", "<MPD xmlns:mas=\"urn:marlin:mas:1-0:services:schemas:mpd\" ");
|
||||||
|
|
||||||
|
return mpdContent;
|
||||||
|
}
|
||||||
|
}
|
@ -1,46 +1,37 @@
|
|||||||
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 string Process(string oriUrl, ParserConfig paserConfig)
|
||||||
{
|
{
|
||||||
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => paserConfig.AppendUrlParams;
|
if (!oriUrl.StartsWith("http")) return oriUrl;
|
||||||
|
|
||||||
|
var uriFromConfig = new Uri(paserConfig.Url);
|
||||||
|
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
|
||||||
|
|
||||||
public override string Process(string oriUrl, ParserConfig paserConfig)
|
var oldUri = new Uri(oriUrl);
|
||||||
|
var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
|
||||||
|
foreach (var item in uriFromConfigQuery.AllKeys)
|
||||||
{
|
{
|
||||||
if (oriUrl.StartsWith("http"))
|
if (newQuery.AllKeys.Contains(item))
|
||||||
{
|
newQuery.Set(item, uriFromConfigQuery.Get(item));
|
||||||
var uriFromConfig = new Uri(paserConfig.Url);
|
else
|
||||||
var uriFromConfigQuery = HttpUtility.ParseQueryString(uriFromConfig.Query);
|
newQuery.Add(item, uriFromConfigQuery.Get(item));
|
||||||
|
|
||||||
var oldUri = new Uri(oriUrl);
|
|
||||||
var newQuery = HttpUtility.ParseQueryString(oldUri.Query);
|
|
||||||
foreach (var item in uriFromConfigQuery.AllKeys)
|
|
||||||
{
|
|
||||||
if (newQuery.AllKeys.Contains(item))
|
|
||||||
newQuery.Set(item, uriFromConfigQuery.Get(item));
|
|
||||||
else
|
|
||||||
newQuery.Add(item, uriFromConfigQuery.Get(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(newQuery.ToString()))
|
|
||||||
{
|
|
||||||
Logger.Debug("Before: " + oriUrl);
|
|
||||||
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery.ToString()).TrimEnd('?');
|
|
||||||
Logger.Debug("After: " + oriUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return oriUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(newQuery.ToString())) return oriUrl;
|
||||||
|
|
||||||
|
Logger.Debug("Before: " + oriUrl);
|
||||||
|
oriUrl = (oldUri.GetLeftPart(UriPartial.Path) + "?" + newQuery).TrimEnd('?');
|
||||||
|
Logger.Debug("After: " + oriUrl);
|
||||||
|
|
||||||
|
return oriUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,108 +1,96 @@
|
|||||||
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=\\\"(.*?)\\\"")]
|
||||||
|
private static partial Regex YkDVRegex();
|
||||||
|
[GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
|
||||||
|
private static partial Regex DNSPRegex();
|
||||||
|
[GeneratedRegex(@"#EXTINF:.*?,\s+.*BUMPER.*\s+?#EXT-X-DISCONTINUITY")]
|
||||||
|
private static partial Regex DNSPSubRegex();
|
||||||
|
[GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
|
||||||
|
private static partial Regex OrderFixRegex();
|
||||||
|
[GeneratedRegex(@"#EXT-X-MAP.*\.apple\.com/")]
|
||||||
|
private static partial Regex ATVRegex();
|
||||||
|
[GeneratedRegex(@"(#EXT-X-KEY:[\s\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
|
||||||
|
private static partial Regex ATVRegex2();
|
||||||
|
|
||||||
|
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
|
||||||
|
|
||||||
|
public override string Process(string m3u8Content, ParserConfig parserConfig)
|
||||||
{
|
{
|
||||||
[GeneratedRegex("#EXT-X-DISCONTINUITY\\s+#EXT-X-MAP:URI=\\\"(.*?)\\\",BYTERANGE=\\\"(.*?)\\\"")]
|
// 处理content以\r作为换行符的情况
|
||||||
private static partial Regex YkDVRegex();
|
if (m3u8Content.Contains('\r') && !m3u8Content.Contains('\n'))
|
||||||
[GeneratedRegex("#EXT-X-MAP:URI=\\\".*?BUMPER/[\\s\\S]+?#EXT-X-DISCONTINUITY")]
|
|
||||||
private static partial Regex DNSPRegex();
|
|
||||||
[GeneratedRegex("#EXTINF:.*?,\\s+.*BUMPER.*\\s+?#EXT-X-DISCONTINUITY")]
|
|
||||||
private static partial Regex DNSPSubRegex();
|
|
||||||
[GeneratedRegex("(#EXTINF.*)(\\s+)(#EXT-X-KEY.*)")]
|
|
||||||
private static partial Regex OrderFixRegex();
|
|
||||||
[GeneratedRegex("#EXT-X-MAP.*\\.apple\\.com/")]
|
|
||||||
private static partial Regex ATVRegex();
|
|
||||||
[GeneratedRegex("(#EXT-X-KEY:[\\s\\S]*?)(#EXT-X-DISCONTINUITY|#EXT-X-ENDLIST)")]
|
|
||||||
private static partial Regex ATVRegex2();
|
|
||||||
|
|
||||||
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS;
|
|
||||||
|
|
||||||
public override string Process(string m3u8Content, ParserConfig parserConfig)
|
|
||||||
{
|
{
|
||||||
//处理content以\r作为换行符的情况
|
m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
|
||||||
if (m3u8Content.Contains("\r") && !m3u8Content.Contains("\n"))
|
|
||||||
{
|
|
||||||
m3u8Content = m3u8Content.Replace("\r", Environment.NewLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
var m3u8Url = parserConfig.Url;
|
|
||||||
//央视频回放
|
|
||||||
if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime="))
|
|
||||||
{
|
|
||||||
m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
|
|
||||||
}
|
|
||||||
|
|
||||||
//IMOOC
|
|
||||||
if (m3u8Url.Contains("imooc.com/"))
|
|
||||||
{
|
|
||||||
//M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
|
|
||||||
}
|
|
||||||
|
|
||||||
//iqy
|
|
||||||
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="))
|
|
||||||
{
|
|
||||||
Regex ykmap = YkDVRegex();
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//针对Disney+修正
|
|
||||||
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
|
|
||||||
{
|
|
||||||
Regex ykmap = DNSPRegex();
|
|
||||||
if (ykmap.IsMatch(m3u8Content))
|
|
||||||
{
|
|
||||||
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//针对Disney+字幕修正
|
|
||||||
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/"))
|
|
||||||
{
|
|
||||||
Regex ykmap = DNSPSubRegex();
|
|
||||||
if (ykmap.IsMatch(m3u8Content))
|
|
||||||
{
|
|
||||||
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//针对AppleTv修正
|
|
||||||
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || ATVRegex().IsMatch(m3u8Content)))
|
|
||||||
{
|
|
||||||
//只取加密部分即可
|
|
||||||
Regex ykmap = ATVRegex2();
|
|
||||||
if (ykmap.IsMatch(m3u8Content))
|
|
||||||
{
|
|
||||||
m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//修复#EXT-X-KEY与#EXTINF出现次序异常问题
|
|
||||||
var regex = OrderFixRegex();
|
|
||||||
if (regex.IsMatch(m3u8Content))
|
|
||||||
{
|
|
||||||
m3u8Content = regex.Replace(m3u8Content, "$3$2$1");
|
|
||||||
}
|
|
||||||
|
|
||||||
return m3u8Content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var m3u8Url = parserConfig.Url;
|
||||||
|
// YSP回放
|
||||||
|
if (m3u8Url.Contains("tlivecloud-playback-cdn.ysp.cctv.cn") && m3u8Url.Contains("endtime="))
|
||||||
|
{
|
||||||
|
m3u8Content += Environment.NewLine + HLSTags.ext_x_endlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMOOC
|
||||||
|
if (m3u8Url.Contains("imooc.com/"))
|
||||||
|
{
|
||||||
|
// M3u8Content = DecodeImooc.DecodeM3u8(M3u8Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 针对YK #EXT-X-VERSION:7杜比视界片源修正
|
||||||
|
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Content.Contains("ott.cibntv.net") && m3u8Content.Contains("ccode="))
|
||||||
|
{
|
||||||
|
var ykmap = YkDVRegex();
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 针对Disney+修正
|
||||||
|
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && m3u8Url.Contains("media.dssott.com/"))
|
||||||
|
{
|
||||||
|
Regex ykmap = DNSPRegex();
|
||||||
|
if (ykmap.IsMatch(m3u8Content))
|
||||||
|
{
|
||||||
|
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 针对Disney+字幕修正
|
||||||
|
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("seg_00000.vtt") && m3u8Url.Contains("media.dssott.com/"))
|
||||||
|
{
|
||||||
|
Regex ykmap = DNSPSubRegex();
|
||||||
|
if (ykmap.IsMatch(m3u8Content))
|
||||||
|
{
|
||||||
|
m3u8Content = m3u8Content.Replace(ykmap.Match(m3u8Content).Value, "#XXX");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 针对AppleTv修正
|
||||||
|
if (m3u8Content.Contains("#EXT-X-DISCONTINUITY") && m3u8Content.Contains("#EXT-X-MAP") && (m3u8Url.Contains(".apple.com/") || ATVRegex().IsMatch(m3u8Content)))
|
||||||
|
{
|
||||||
|
// 只取加密部分即可
|
||||||
|
Regex ykmap = ATVRegex2();
|
||||||
|
if (ykmap.IsMatch(m3u8Content))
|
||||||
|
{
|
||||||
|
m3u8Content = "#EXTM3U\r\n" + ykmap.Match(m3u8Content).Groups[1].Value + "\r\n#EXT-X-ENDLIST";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复#EXT-X-KEY与#EXTINF出现次序异常问题
|
||||||
|
var regex = OrderFixRegex();
|
||||||
|
if (regex.IsMatch(m3u8Content))
|
||||||
|
{
|
||||||
|
m3u8Content = regex.Replace(m3u8Content, "$3$2$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return m3u8Content;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,112 +6,105 @@ 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 EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
|
||||||
{
|
{
|
||||||
public override bool CanProcess(ExtractorType extractorType, string m3u8Url, string keyLine, string m3u8Content, ParserConfig paserConfig) => extractorType == ExtractorType.HLS;
|
var iv = ParserUtil.GetAttribute(keyLine, "IV");
|
||||||
|
var method = ParserUtil.GetAttribute(keyLine, "METHOD");
|
||||||
|
var uri = ParserUtil.GetAttribute(keyLine, "URI");
|
||||||
|
|
||||||
|
Logger.Debug("METHOD:{},URI:{},IV:{}", method, uri, iv);
|
||||||
|
|
||||||
public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
|
var encryptInfo = new EncryptInfo(method);
|
||||||
|
|
||||||
|
// IV
|
||||||
|
if (!string.IsNullOrEmpty(iv))
|
||||||
{
|
{
|
||||||
var iv = ParserUtil.GetAttribute(keyLine, "IV");
|
encryptInfo.IV = HexUtil.HexToBytes(iv);
|
||||||
var method = ParserUtil.GetAttribute(keyLine, "METHOD");
|
}
|
||||||
var uri = ParserUtil.GetAttribute(keyLine, "URI");
|
// 自定义IV
|
||||||
|
if (parserConfig.CustomeIV is { Length: > 0 })
|
||||||
|
{
|
||||||
|
encryptInfo.IV = parserConfig.CustomeIV;
|
||||||
|
}
|
||||||
|
|
||||||
Logger.Debug("METHOD:{},URI:{},IV:{}", method, uri, iv);
|
// KEY
|
||||||
|
try
|
||||||
var encryptInfo = new EncryptInfo(method);
|
{
|
||||||
|
if (parserConfig.CustomeKey is { Length: > 0 })
|
||||||
//IV
|
|
||||||
if (!string.IsNullOrEmpty(iv))
|
|
||||||
{
|
{
|
||||||
encryptInfo.IV = HexUtil.HexToBytes(iv);
|
encryptInfo.Key = parserConfig.CustomeKey;
|
||||||
}
|
}
|
||||||
//自定义IV
|
else if (uri.ToLower().StartsWith("base64:"))
|
||||||
if (parserConfig.CustomeIV != null && parserConfig.CustomeIV.Length > 0)
|
|
||||||
{
|
{
|
||||||
encryptInfo.IV = parserConfig.CustomeIV;
|
encryptInfo.Key = Convert.FromBase64String(uri[7..]);
|
||||||
}
|
}
|
||||||
|
else if (uri.ToLower().StartsWith("data:;base64,"))
|
||||||
//KEY
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (parserConfig.CustomeKey != null && parserConfig.CustomeKey.Length > 0)
|
encryptInfo.Key = Convert.FromBase64String(uri[13..]);
|
||||||
{
|
}
|
||||||
encryptInfo.Key = parserConfig.CustomeKey;
|
else if (uri.ToLower().StartsWith("data:text/plain;base64,"))
|
||||||
}
|
{
|
||||||
else if (uri.ToLower().StartsWith("base64:"))
|
encryptInfo.Key = Convert.FromBase64String(uri[23..]);
|
||||||
{
|
}
|
||||||
encryptInfo.Key = Convert.FromBase64String(uri[7..]);
|
else if (File.Exists(uri))
|
||||||
}
|
{
|
||||||
else if (uri.ToLower().StartsWith("data:;base64,"))
|
encryptInfo.Key = File.ReadAllBytes(uri);
|
||||||
{
|
}
|
||||||
encryptInfo.Key = Convert.FromBase64String(uri[13..]);
|
else if (!string.IsNullOrEmpty(uri))
|
||||||
}
|
{
|
||||||
else if (uri.ToLower().StartsWith("data:text/plain;base64,"))
|
var retryCount = parserConfig.KeyRetryCount;
|
||||||
{
|
var segUrl = PreProcessUrl(ParserUtil.CombineURL(m3u8Url, uri), parserConfig);
|
||||||
encryptInfo.Key = Convert.FromBase64String(uri[23..]);
|
|
||||||
}
|
|
||||||
else if (File.Exists(uri))
|
|
||||||
{
|
|
||||||
encryptInfo.Key = File.ReadAllBytes(uri);
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(uri))
|
|
||||||
{
|
|
||||||
var retryCount = parserConfig.KeyRetryCount;
|
|
||||||
var segUrl = PreProcessUrl(ParserUtil.CombineURL(m3u8Url, uri), parserConfig);
|
|
||||||
getHttpKey:
|
getHttpKey:
|
||||||
try
|
try
|
||||||
{
|
|
||||||
var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;
|
|
||||||
encryptInfo.Key = bytes;
|
|
||||||
}
|
|
||||||
catch (Exception _ex) when (!_ex.Message.Contains("scheme is not supported."))
|
|
||||||
{
|
|
||||||
Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
if (retryCount-- > 0) goto getHttpKey;
|
|
||||||
else throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message);
|
|
||||||
encryptInfo.Method = EncryptMethod.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
//处理自定义加密方式
|
|
||||||
if (parserConfig.CustomMethod != null)
|
|
||||||
{
|
|
||||||
encryptInfo.Method = parserConfig.CustomMethod.Value;
|
|
||||||
Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
|
|
||||||
}
|
|
||||||
|
|
||||||
return encryptInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 预处理URL
|
|
||||||
/// </summary>
|
|
||||||
private string PreProcessUrl(string url, ParserConfig parserConfig)
|
|
||||||
{
|
|
||||||
foreach (var p in parserConfig.UrlProcessors)
|
|
||||||
{
|
|
||||||
if (p.CanProcess(ExtractorType.HLS, url, parserConfig))
|
|
||||||
{
|
{
|
||||||
url = p.Process(url, parserConfig);
|
var bytes = HTTPUtil.GetBytesAsync(segUrl, parserConfig.Headers).Result;
|
||||||
|
encryptInfo.Key = bytes;
|
||||||
|
}
|
||||||
|
catch (Exception _ex) when (!_ex.Message.Contains("scheme is not supported."))
|
||||||
|
{
|
||||||
|
Logger.WarnMarkUp($"[grey]{_ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
if (retryCount-- > 0) goto getHttpKey;
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ResString.cmd_loadKeyFailed + ": " + ex.Message);
|
||||||
|
encryptInfo.Method = EncryptMethod.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parserConfig.CustomMethod == null) return encryptInfo;
|
||||||
|
|
||||||
|
// 处理自定义加密方式
|
||||||
|
encryptInfo.Method = parserConfig.CustomMethod.Value;
|
||||||
|
Logger.Warn("METHOD changed from {} to {}", method, encryptInfo.Method);
|
||||||
|
|
||||||
|
return encryptInfo;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预处理URL
|
||||||
|
/// </summary>
|
||||||
|
private string PreProcessUrl(string url, ParserConfig parserConfig)
|
||||||
|
{
|
||||||
|
foreach (var p in parserConfig.UrlProcessors)
|
||||||
|
{
|
||||||
|
if (p.CanProcess(ExtractorType.HLS, url, parserConfig))
|
||||||
|
{
|
||||||
|
url = p.Process(url, parserConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
@ -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 EncryptInfo Process(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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 string Process(string oriUrl, ParserConfig parserConfig);
|
||||||
public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig);
|
}
|
||||||
public abstract string Process(string oriUrl, ParserConfig parserConfig);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,144 +7,138 @@ 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;
|
||||||
|
private IExtractor extractor;
|
||||||
|
private ParserConfig parserConfig = new();
|
||||||
|
private string rawText;
|
||||||
|
private static SemaphoreSlim semaphore = new(1, 1);
|
||||||
|
|
||||||
|
public Dictionary<string, string> RawFiles { get; set; } = new(); // 存储(文件名,文件内容)
|
||||||
|
|
||||||
|
public StreamExtractor(ParserConfig parserConfig)
|
||||||
{
|
{
|
||||||
public ExtractorType ExtractorType { get => extractor.ExtractorType; }
|
this.parserConfig = parserConfig;
|
||||||
private IExtractor extractor;
|
}
|
||||||
private ParserConfig parserConfig = new ParserConfig();
|
|
||||||
private string rawText;
|
|
||||||
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
|
|
||||||
|
|
||||||
public Dictionary<string, string> RawFiles { get; set; } = new(); //存储(文件名,文件内容)
|
public async Task LoadSourceFromUrlAsync(string url)
|
||||||
|
{
|
||||||
public StreamExtractor()
|
Logger.Info(ResString.loadingUrl + url);
|
||||||
|
if (url.StartsWith("file:"))
|
||||||
{
|
{
|
||||||
|
var uri = new Uri(url);
|
||||||
|
this.rawText = await File.ReadAllTextAsync(uri.LocalPath);
|
||||||
|
parserConfig.OriginalUrl = parserConfig.Url = url;
|
||||||
|
}
|
||||||
|
else if (url.StartsWith("http"))
|
||||||
|
{
|
||||||
|
parserConfig.OriginalUrl = url;
|
||||||
|
(this.rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers);
|
||||||
|
parserConfig.Url = url;
|
||||||
|
}
|
||||||
|
else if (File.Exists(url))
|
||||||
|
{
|
||||||
|
url = Path.GetFullPath(url);
|
||||||
|
this.rawText = await File.ReadAllTextAsync(url);
|
||||||
|
parserConfig.OriginalUrl = parserConfig.Url = new Uri(url).AbsoluteUri;
|
||||||
|
}
|
||||||
|
this.rawText = rawText.Trim();
|
||||||
|
LoadSourceFromText(this.rawText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(this.rawText), nameof(this.extractor))]
|
||||||
|
private void LoadSourceFromText(string rawText)
|
||||||
|
{
|
||||||
|
var rawType = "txt";
|
||||||
|
rawText = rawText.Trim();
|
||||||
|
this.rawText = rawText;
|
||||||
|
if (rawText.StartsWith(HLSTags.ext_m3u))
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp(ResString.matchHLS);
|
||||||
|
extractor = new HLSExtractor(parserConfig);
|
||||||
|
rawType = "m3u8";
|
||||||
|
}
|
||||||
|
else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD"))
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp(ResString.matchDASH);
|
||||||
|
// extractor = new DASHExtractor(parserConfig);
|
||||||
|
extractor = new DASHExtractor2(parserConfig);
|
||||||
|
rawType = "mpd";
|
||||||
|
}
|
||||||
|
else if (rawText.Contains("</SmoothStreamingMedia>") && rawText.Contains("<SmoothStreamingMedia"))
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp(ResString.matchMSS);
|
||||||
|
// extractor = new DASHExtractor(parserConfig);
|
||||||
|
extractor = new MSSExtractor(parserConfig);
|
||||||
|
rawType = "ism";
|
||||||
|
}
|
||||||
|
else if (rawText == ResString.ReLiveTs)
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp(ResString.matchTS);
|
||||||
|
extractor = new LiveTSExtractor(parserConfig);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException(ResString.notSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StreamExtractor(ParserConfig parserConfig)
|
RawFiles[$"raw.{rawType}"] = rawText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始解析流媒体信息
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<List<StreamSpec>> ExtractStreamsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
this.parserConfig = parserConfig;
|
await semaphore.WaitAsync();
|
||||||
|
Logger.Info(ResString.parsingStream);
|
||||||
|
return await extractor.ExtractStreamsAsync(rawText);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
public async Task LoadSourceFromUrlAsync(string url)
|
|
||||||
{
|
{
|
||||||
Logger.Info(ResString.loadingUrl + url);
|
semaphore.Release();
|
||||||
if (url.StartsWith("file:"))
|
|
||||||
{
|
|
||||||
var uri = new Uri(url);
|
|
||||||
this.rawText = await File.ReadAllTextAsync(uri.LocalPath);
|
|
||||||
parserConfig.OriginalUrl = parserConfig.Url = url;
|
|
||||||
}
|
|
||||||
else if (url.StartsWith("http"))
|
|
||||||
{
|
|
||||||
parserConfig.OriginalUrl = url;
|
|
||||||
(this.rawText, url) = await HTTPUtil.GetWebSourceAndNewUrlAsync(url, parserConfig.Headers);
|
|
||||||
parserConfig.Url = url;
|
|
||||||
}
|
|
||||||
else if (File.Exists(url))
|
|
||||||
{
|
|
||||||
url = Path.GetFullPath(url);
|
|
||||||
this.rawText = await File.ReadAllTextAsync(url);
|
|
||||||
parserConfig.OriginalUrl = parserConfig.Url = new Uri(url).AbsoluteUri;
|
|
||||||
}
|
|
||||||
this.rawText = rawText.Trim();
|
|
||||||
LoadSourceFromText(this.rawText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadSourceFromText(string rawText)
|
|
||||||
{
|
|
||||||
var rawType = "txt";
|
|
||||||
rawText = rawText.Trim();
|
|
||||||
this.rawText = rawText;
|
|
||||||
if (rawText.StartsWith(HLSTags.ext_m3u))
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp(ResString.matchHLS);
|
|
||||||
extractor = new HLSExtractor(parserConfig);
|
|
||||||
rawType = "m3u8";
|
|
||||||
}
|
|
||||||
else if (rawText.Contains("</MPD>") && rawText.Contains("<MPD"))
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp(ResString.matchDASH);
|
|
||||||
//extractor = new DASHExtractor(parserConfig);
|
|
||||||
extractor = new DASHExtractor2(parserConfig);
|
|
||||||
rawType = "mpd";
|
|
||||||
}
|
|
||||||
else if (rawText.Contains("</SmoothStreamingMedia>") && rawText.Contains("<SmoothStreamingMedia"))
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp(ResString.matchMSS);
|
|
||||||
//extractor = new DASHExtractor(parserConfig);
|
|
||||||
extractor = new MSSExtractor(parserConfig);
|
|
||||||
rawType = "ism";
|
|
||||||
}
|
|
||||||
else if (rawText == ResString.ReLiveTs)
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp(ResString.matchTS);
|
|
||||||
extractor = new LiveTSExtractor(parserConfig);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new NotSupportedException(ResString.notSupported);
|
|
||||||
}
|
|
||||||
|
|
||||||
RawFiles[$"raw.{rawType}"] = rawText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 开始解析流媒体信息
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<List<StreamSpec>> ExtractStreamsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await semaphore.WaitAsync();
|
|
||||||
Logger.Info(ResString.parsingStream);
|
|
||||||
return await extractor.ExtractStreamsAsync(rawText);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 根据规格说明填充媒体播放列表信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="streamSpecs"></param>
|
|
||||||
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await semaphore.WaitAsync();
|
|
||||||
Logger.Info(ResString.parsingStream);
|
|
||||||
await extractor.FetchPlayListAsync(streamSpecs);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await semaphore.WaitAsync();
|
|
||||||
await RetryUtil.WebRequestRetryAsync(async () =>
|
|
||||||
{
|
|
||||||
await extractor.RefreshPlayListAsync(streamSpecs);
|
|
||||||
return true;
|
|
||||||
}, retryDelayMilliseconds: 1000, maxRetries: 5);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
semaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据规格说明填充媒体播放列表信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="streamSpecs"></param>
|
||||||
|
public async Task FetchPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
Logger.Info(ResString.parsingStream);
|
||||||
|
await extractor.FetchPlayListAsync(streamSpecs);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshPlayListAsync(List<StreamSpec> streamSpecs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
await RetryUtil.WebRequestRetryAsync(async () =>
|
||||||
|
{
|
||||||
|
await extractor.RefreshPlayListAsync(streamSpecs);
|
||||||
|
return true;
|
||||||
|
}, retryDelayMilliseconds: 1000, maxRetries: 5);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,126 +1,114 @@
|
|||||||
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\$")]
|
||||||
|
private static partial Regex VarsNumberRegex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从以下文本中获取参数
|
||||||
|
/// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="line">等待被解析的一行文本</param>
|
||||||
|
/// <param name="key">留空则获取第一个英文冒号后的全部字符</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetAttribute(string line, string key = "")
|
||||||
{
|
{
|
||||||
[GeneratedRegex("\\$Number%([^$]+)d\\$")]
|
line = line.Trim();
|
||||||
private static partial Regex VarsNumberRegex();
|
if (key == "")
|
||||||
|
return line[(line.IndexOf(':') + 1)..];
|
||||||
|
|
||||||
/// <summary>
|
var index = -1;
|
||||||
/// 从以下文本中获取参数
|
var result = string.Empty;
|
||||||
/// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720"
|
if ((index = line.IndexOf(key + "=\"", StringComparison.Ordinal)) > -1)
|
||||||
/// </summary>
|
|
||||||
/// <param name="line">等待被解析的一行文本</param>
|
|
||||||
/// <param name="key">留空则获取第一个英文冒号后的全部字符</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string GetAttribute(string line, string key = "")
|
|
||||||
{
|
{
|
||||||
line = line.Trim();
|
var startIndex = index + (key + "=\"").Length;
|
||||||
if (key == "")
|
var endIndex = startIndex + line[startIndex..].IndexOf('\"');
|
||||||
return line[(line.IndexOf(':') + 1)..];
|
result = line[startIndex..endIndex];
|
||||||
|
}
|
||||||
var index = -1;
|
else if ((index = line.IndexOf(key + "=", StringComparison.Ordinal)) > -1)
|
||||||
var result = string.Empty;
|
{
|
||||||
if ((index = line.IndexOf(key + "=\"")) > -1)
|
var startIndex = index + (key + "=").Length;
|
||||||
{
|
var endIndex = startIndex + line[startIndex..].IndexOf(',');
|
||||||
var startIndex = index + (key + "=\"").Length;
|
result = endIndex >= startIndex ? line[startIndex..endIndex] : line[startIndex..];
|
||||||
var endIndex = startIndex + line[startIndex..].IndexOf('\"');
|
|
||||||
result = line[startIndex..endIndex];
|
|
||||||
}
|
|
||||||
else if ((index = line.IndexOf(key + "=")) > -1)
|
|
||||||
{
|
|
||||||
var startIndex = index + (key + "=").Length;
|
|
||||||
var endIndex = startIndex + line[startIndex..].IndexOf(',');
|
|
||||||
if (endIndex >= startIndex) result = line[startIndex..endIndex];
|
|
||||||
else result = line[startIndex..];
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return result;
|
||||||
/// 从如下文本中提取
|
|
||||||
/// <n>[@<o>]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input"></param>
|
|
||||||
/// <returns>n(length) o(start)</returns>
|
|
||||||
public static (long, long?) GetRange(string input)
|
|
||||||
{
|
|
||||||
var t = input.Split('@');
|
|
||||||
if (t.Length > 0)
|
|
||||||
{
|
|
||||||
if (t.Length == 1)
|
|
||||||
{
|
|
||||||
return (Convert.ToInt64(t[0]), null);
|
|
||||||
}
|
|
||||||
if (t.Length == 2)
|
|
||||||
{
|
|
||||||
return (Convert.ToInt64(t[0]), Convert.ToInt64(t[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (0, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从100-300这种字符串中获取StartRange, ExpectLength信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="range"></param>
|
|
||||||
/// <returns>StartRange, ExpectLength</returns>
|
|
||||||
public static (long, long) ParseRange(string range)
|
|
||||||
{
|
|
||||||
var start = Convert.ToInt64(range.Split('-')[0]);
|
|
||||||
var end = Convert.ToInt64(range.Split('-')[1]);
|
|
||||||
return (start, end - start + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MPD SegmentTemplate替换
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="text"></param>
|
|
||||||
/// <param name="keyValuePairs"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ReplaceVars(string text, Dictionary<string, object?> keyValuePairs)
|
|
||||||
{
|
|
||||||
foreach (var item in keyValuePairs)
|
|
||||||
if (text.Contains(item.Key))
|
|
||||||
text = text.Replace(item.Key, item.Value!.ToString());
|
|
||||||
|
|
||||||
//处理特殊形式数字 如 $Number%05d$
|
|
||||||
var regex = VarsNumberRegex();
|
|
||||||
if (regex.IsMatch(text) && keyValuePairs.ContainsKey(DASHTags.TemplateNumber))
|
|
||||||
{
|
|
||||||
foreach (Match m in regex.Matches(text))
|
|
||||||
{
|
|
||||||
text = text.Replace(m.Value, keyValuePairs[DASHTags.TemplateNumber]?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 拼接Baseurl和RelativeUrl
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="baseurl">Baseurl</param>
|
|
||||||
/// <param name="url">RelativeUrl</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string CombineURL(string baseurl, string url)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(baseurl))
|
|
||||||
return url;
|
|
||||||
|
|
||||||
Uri uri1 = new Uri(baseurl); //这里直接传完整的URL即可
|
|
||||||
Uri uri2 = new Uri(uri1, url);
|
|
||||||
url = uri2.ToString();
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从如下文本中提取
|
||||||
|
/// <n>[@<o>]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns>n(length) o(start)</returns>
|
||||||
|
public static (long, long?) GetRange(string input)
|
||||||
|
{
|
||||||
|
var t = input.Split('@');
|
||||||
|
return t.Length switch
|
||||||
|
{
|
||||||
|
<= 0 => (0, null),
|
||||||
|
1 => (Convert.ToInt64(t[0]), null),
|
||||||
|
2 => (Convert.ToInt64(t[0]), Convert.ToInt64(t[1])),
|
||||||
|
_ => (0, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从100-300这种字符串中获取StartRange, ExpectLength信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="range"></param>
|
||||||
|
/// <returns>StartRange, ExpectLength</returns>
|
||||||
|
public static (long, long) ParseRange(string range)
|
||||||
|
{
|
||||||
|
var start = Convert.ToInt64(range.Split('-')[0]);
|
||||||
|
var end = Convert.ToInt64(range.Split('-')[1]);
|
||||||
|
return (start, end - start + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MPD SegmentTemplate替换
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text"></param>
|
||||||
|
/// <param name="keyValuePairs"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ReplaceVars(string text, Dictionary<string, object?> keyValuePairs)
|
||||||
|
{
|
||||||
|
foreach (var item in keyValuePairs)
|
||||||
|
if (text.Contains(item.Key))
|
||||||
|
text = text.Replace(item.Key, item.Value!.ToString());
|
||||||
|
|
||||||
|
// 处理特殊形式数字 如 $Number%05d$
|
||||||
|
var regex = VarsNumberRegex();
|
||||||
|
if (regex.IsMatch(text) && keyValuePairs.TryGetValue(DASHTags.TemplateNumber, out var keyValuePair))
|
||||||
|
{
|
||||||
|
foreach (Match m in regex.Matches(text))
|
||||||
|
{
|
||||||
|
text = text.Replace(m.Value, keyValuePair?.ToString()?.PadLeft(Convert.ToInt32(m.Groups[1].Value), '0'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 拼接Baseurl和RelativeUrl
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseurl">Baseurl</param>
|
||||||
|
/// <param name="url">RelativeUrl</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CombineURL(string baseurl, string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(baseurl))
|
||||||
|
return url;
|
||||||
|
|
||||||
|
var uri1 = new Uri(baseurl); // 这里直接传完整的URL即可
|
||||||
|
var uri2 = new Uri(uri1, url);
|
||||||
|
url = uri2.ToString();
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
@ -1,66 +1,48 @@
|
|||||||
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 ConcurrentDictionary<int, string> DateTimeStringDic = new();
|
||||||
|
protected override bool NoWrap => true;
|
||||||
|
private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
|
||||||
|
|
||||||
|
public DownloadSpeedColumn(ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic)
|
||||||
{
|
{
|
||||||
private long _stopSpeed = 0;
|
this.SpeedContainerDic = SpeedContainerDic;
|
||||||
private ConcurrentDictionary<int, string> DateTimeStringDic = new();
|
}
|
||||||
protected override bool NoWrap => true;
|
|
||||||
private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
|
|
||||||
|
|
||||||
public DownloadSpeedColumn(ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic)
|
public Style MyStyle { get; set; } = new Style(foreground: Color.Green);
|
||||||
|
|
||||||
|
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
var taskId = task.Id;
|
||||||
|
var speedContainer = SpeedContainerDic[taskId];
|
||||||
|
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
var flag = task.IsFinished || !task.IsStarted;
|
||||||
|
// 单文件下载汇报进度
|
||||||
|
if (!flag && speedContainer is { SingleSegment: true, ResponseLength: not null })
|
||||||
{
|
{
|
||||||
this.SpeedContainerDic = SpeedContainerDic;
|
task.MaxValue = (double)speedContainer.ResponseLength;
|
||||||
|
task.Value = speedContainer.RDownloaded;
|
||||||
}
|
}
|
||||||
|
// 一秒汇报一次即可
|
||||||
public Style MyStyle { get; set; } = new Style(foreground: Color.Green);
|
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
|
||||||
|
|
||||||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
|
||||||
{
|
{
|
||||||
var taskId = task.Id;
|
speedContainer.NowSpeed = speedContainer.Downloaded;
|
||||||
var speedContainer = SpeedContainerDic[taskId];
|
// 速度为0,计数增加
|
||||||
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
|
||||||
var flag = task.IsFinished || !task.IsStarted;
|
else speedContainer.ResetLowSpeedCount();
|
||||||
//单文件下载汇报进度
|
speedContainer.Reset();
|
||||||
if (!flag && speedContainer.SingleSegment && speedContainer.ResponseLength != null)
|
|
||||||
{
|
|
||||||
task.MaxValue = (double)speedContainer.ResponseLength;
|
|
||||||
task.Value = speedContainer.RDownloaded;
|
|
||||||
}
|
|
||||||
//一秒汇报一次即可
|
|
||||||
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now && !flag)
|
|
||||||
{
|
|
||||||
speedContainer.NowSpeed = speedContainer.Downloaded;
|
|
||||||
//速度为0,计数增加
|
|
||||||
if (speedContainer.Downloaded <= _stopSpeed) { speedContainer.AddLowSpeedCount(); }
|
|
||||||
else speedContainer.ResetLowSpeedCount();
|
|
||||||
speedContainer.Reset();
|
|
||||||
}
|
|
||||||
DateTimeStringDic[taskId] = now;
|
|
||||||
var style = flag ? Style.Plain : MyStyle;
|
|
||||||
return flag ? new Text("-", style).Centered() : new Text(FormatFileSize(speedContainer.NowSpeed) + (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)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
DateTimeStringDic[taskId] = now;
|
||||||
|
var style = flag ? Style.Plain : MyStyle;
|
||||||
|
return flag ? new Text("-", style).Centered() : new Text(GlobalUtil.FormatFileSize(speedContainer.NowSpeed) + "ps" + (speedContainer.LowSpeedCount > 0 ? $"({speedContainer.LowSpeedCount})" : ""), style).Centered();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,48 +2,42 @@
|
|||||||
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, string> DateTimeStringDic = new();
|
||||||
|
private ConcurrentDictionary<int, string> SizeDic = new();
|
||||||
|
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
|
||||||
|
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
|
||||||
|
|
||||||
|
public DownloadStatusColumn(ConcurrentDictionary<int, SpeedContainer> speedContainerDic)
|
||||||
{
|
{
|
||||||
private ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic { get; set; }
|
this.SpeedContainerDic = speedContainerDic;
|
||||||
private ConcurrentDictionary<int, string> DateTimeStringDic = new();
|
|
||||||
private ConcurrentDictionary<int, string> SizeDic = new();
|
|
||||||
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
|
|
||||||
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Green);
|
|
||||||
|
|
||||||
public DownloadStatusColumn(ConcurrentDictionary<int, SpeedContainer> speedContainerDic)
|
|
||||||
{
|
|
||||||
this.SpeedContainerDic = speedContainerDic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
|
||||||
{
|
|
||||||
if (task.Value == 0) return new Text("-", MyStyle).RightJustified();
|
|
||||||
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
||||||
|
|
||||||
var speedContainer = SpeedContainerDic[task.Id];
|
|
||||||
var size = speedContainer.RDownloaded;
|
|
||||||
|
|
||||||
//一秒汇报一次即可
|
|
||||||
if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
|
|
||||||
{
|
|
||||||
var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
|
|
||||||
SizeDic[task.Id] = $"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}";
|
|
||||||
}
|
|
||||||
DateTimeStringDic[task.Id] = now;
|
|
||||||
SizeDic.TryGetValue(task.Id, out var sizeStr);
|
|
||||||
|
|
||||||
if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);
|
|
||||||
|
|
||||||
return new Text(sizeStr ?? "-", MyStyle).RightJustified();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
if (task.Value == 0) return new Text("-", MyStyle).RightJustified();
|
||||||
|
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
var speedContainer = SpeedContainerDic[task.Id];
|
||||||
|
var size = speedContainer.RDownloaded;
|
||||||
|
|
||||||
|
// 一秒汇报一次即可
|
||||||
|
if (DateTimeStringDic.TryGetValue(task.Id, out var oldTime) && oldTime != now)
|
||||||
|
{
|
||||||
|
var totalSize = speedContainer.SingleSegment ? (speedContainer.ResponseLength ?? 0) : (long)(size / (task.Value / task.MaxValue));
|
||||||
|
SizeDic[task.Id] = $"{GlobalUtil.FormatFileSize(size)}/{GlobalUtil.FormatFileSize(totalSize)}";
|
||||||
|
}
|
||||||
|
DateTimeStringDic[task.Id] = now;
|
||||||
|
SizeDic.TryGetValue(task.Id, out var sizeStr);
|
||||||
|
|
||||||
|
if (task.IsFinished) sizeStr = GlobalUtil.FormatFileSize(size);
|
||||||
|
|
||||||
|
return new Text(sizeStr ?? "-", MyStyle).RightJustified();
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,25 @@
|
|||||||
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>
|
||||||
|
/// Gets or sets the style for a non-complete task.
|
||||||
|
/// </summary>
|
||||||
|
public Style Style { get; set; } = Style.Plain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the style for a completed task.
|
||||||
|
/// </summary>
|
||||||
|
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
||||||
{
|
{
|
||||||
/// <summary>
|
var percentage = task.Percentage;
|
||||||
/// Gets or sets the style for a non-complete task.
|
var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
|
||||||
/// </summary>
|
return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
|
||||||
public Style Style { get; set; } = Style.Plain;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the style for a completed task.
|
|
||||||
/// </summary>
|
|
||||||
public Style CompletedStyle { get; set; } = new Style(foreground: Color.Green);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
|
||||||
{
|
|
||||||
var percentage = task.Percentage;
|
|
||||||
var style = percentage == 100 ? CompletedStyle : Style ?? Style.Plain;
|
|
||||||
return new Text($"{task.Value}/{task.MaxValue} {percentage:F2}%", style).RightJustified();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,39 +1,30 @@
|
|||||||
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;
|
||||||
|
private ConcurrentDictionary<int, int> _recodingDurDic;
|
||||||
|
private ConcurrentDictionary<int, int>? _refreshedDurDic;
|
||||||
|
public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey);
|
||||||
|
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen);
|
||||||
|
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic)
|
||||||
{
|
{
|
||||||
protected override bool NoWrap => true;
|
_recodingDurDic = recodingDurDic;
|
||||||
private ConcurrentDictionary<int, int> _recodingDurDic;
|
|
||||||
private ConcurrentDictionary<int, int>? _refreshedDurDic;
|
|
||||||
public Style GreyStyle { get; set; } = new Style(foreground: Color.Grey);
|
|
||||||
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkGreen);
|
|
||||||
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic)
|
|
||||||
{
|
|
||||||
_recodingDurDic = recodingDurDic;
|
|
||||||
}
|
|
||||||
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic, ConcurrentDictionary<int, int> refreshedDurDic)
|
|
||||||
{
|
|
||||||
_recodingDurDic = recodingDurDic;
|
|
||||||
_refreshedDurDic = refreshedDurDic;
|
|
||||||
}
|
|
||||||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
|
||||||
{
|
|
||||||
if (_refreshedDurDic == null)
|
|
||||||
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
public RecordingDurationColumn(ConcurrentDictionary<int, int> recodingDurDic, ConcurrentDictionary<int, int> refreshedDurDic)
|
||||||
|
{
|
||||||
|
_recodingDurDic = recodingDurDic;
|
||||||
|
_refreshedDurDic = refreshedDurDic;
|
||||||
|
}
|
||||||
|
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
if (_refreshedDurDic == null)
|
||||||
|
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}", MyStyle).LeftJustified();
|
||||||
|
return new Text($"{GlobalUtil.FormatTime(_recodingDurDic[task.Id])}/{GlobalUtil.FormatTime(_refreshedDurDic[task.Id])}", GreyStyle);
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +1,32 @@
|
|||||||
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;
|
||||||
|
private ConcurrentDictionary<int, double> RecodingSizeDic = new(); // 临时的大小 每秒刷新用
|
||||||
|
private ConcurrentDictionary<int, double> _recodingSizeDic;
|
||||||
|
private ConcurrentDictionary<int, string> DateTimeStringDic = new();
|
||||||
|
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
|
||||||
|
public RecordingSizeColumn(ConcurrentDictionary<int, double> recodingSizeDic)
|
||||||
{
|
{
|
||||||
protected override bool NoWrap => true;
|
_recodingSizeDic = recodingSizeDic;
|
||||||
private ConcurrentDictionary<int, double> RecodingSizeDic = new(); //临时的大小 每秒刷新用
|
|
||||||
private ConcurrentDictionary<int, double> _recodingSizeDic;
|
|
||||||
private ConcurrentDictionary<int, string> DateTimeStringDic = new();
|
|
||||||
public Style MyStyle { get; set; } = new Style(foreground: Color.DarkCyan);
|
|
||||||
public RecordingSizeColumn(ConcurrentDictionary<int, double> recodingSizeDic)
|
|
||||||
{
|
|
||||||
_recodingSizeDic = recodingSizeDic;
|
|
||||||
}
|
|
||||||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
|
||||||
{
|
|
||||||
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
||||||
var taskId = task.Id;
|
|
||||||
//一秒汇报一次即可
|
|
||||||
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
|
|
||||||
{
|
|
||||||
RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
|
|
||||||
}
|
|
||||||
DateTimeStringDic[taskId] = now;
|
|
||||||
var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
|
|
||||||
return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
||||||
|
{
|
||||||
|
var now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
var taskId = task.Id;
|
||||||
|
// 一秒汇报一次即可
|
||||||
|
if (DateTimeStringDic.TryGetValue(taskId, out var oldTime) && oldTime != now)
|
||||||
|
{
|
||||||
|
RecodingSizeDic[task.Id] = _recodingSizeDic[task.Id];
|
||||||
|
}
|
||||||
|
DateTimeStringDic[taskId] = now;
|
||||||
|
var flag = RecodingSizeDic.TryGetValue(taskId, out var size);
|
||||||
|
return new Text(GlobalUtil.FormatFileSize(flag ? size : 0), MyStyle).LeftJustified();
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,17 @@
|
|||||||
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;
|
||||||
|
public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
|
||||||
|
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
|
||||||
|
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
||||||
{
|
{
|
||||||
protected override bool NoWrap => true;
|
if (task.IsFinished)
|
||||||
public Style MyStyle { get; set; } = new Style(foreground: Color.Default);
|
return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified();
|
||||||
public Style FinishedStyle { get; set; } = new Style(foreground: Color.Yellow);
|
return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
|
||||||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime)
|
|
||||||
{
|
|
||||||
if (task.IsFinished)
|
|
||||||
return new Text($"{task.Value}/{task.MaxValue} Waiting ", FinishedStyle).LeftJustified();
|
|
||||||
return new Text($"{task.Value}/{task.MaxValue} Recording", MyStyle).LeftJustified();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,61 +1,56 @@
|
|||||||
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;
|
||||||
|
public ComplexParamParser(string arg)
|
||||||
{
|
{
|
||||||
private string _arg;
|
_arg = arg;
|
||||||
public ComplexParamParser(string arg)
|
}
|
||||||
{
|
|
||||||
_arg = arg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? GetValue(string key)
|
public string? GetValue(string key)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(_arg)) return null;
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(_arg)) return null;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
var index = _arg.IndexOf(key + "=", StringComparison.Ordinal);
|
||||||
|
if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;
|
||||||
|
|
||||||
|
var chars = _arg[(index + key.Length + 1)..].ToCharArray();
|
||||||
|
var result = new StringBuilder();
|
||||||
|
char last = '\0';
|
||||||
|
for (int i = 0; i < chars.Length; i++)
|
||||||
{
|
{
|
||||||
var index = _arg.IndexOf(key + "=");
|
if (chars[i] == ':')
|
||||||
if (index == -1) return (_arg.Contains(key) && _arg.EndsWith(key)) ? "true" : null;
|
|
||||||
|
|
||||||
var chars = _arg[(index + key.Length + 1)..].ToCharArray();
|
|
||||||
var result = new StringBuilder();
|
|
||||||
char last = '\0';
|
|
||||||
for (int i = 0; i < chars.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (chars[i] == ':')
|
if (last == '\\')
|
||||||
{
|
|
||||||
if (last == '\\')
|
|
||||||
{
|
|
||||||
result.Replace("\\", "");
|
|
||||||
last = chars[i];
|
|
||||||
result.Append(chars[i]);
|
|
||||||
}
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
result.Replace("\\", "");
|
||||||
last = chars[i];
|
last = chars[i];
|
||||||
result.Append(chars[i]);
|
result.Append(chars[i]);
|
||||||
}
|
}
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
last = chars[i];
|
||||||
|
result.Append(chars[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultStr = result.ToString().Trim().Trim('\"').Trim('\'');
|
|
||||||
|
|
||||||
//不应该有引号出现
|
|
||||||
if (resultStr.Contains('\"') || resultStr.Contains('\'')) throw new Exception();
|
|
||||||
|
|
||||||
return resultStr;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Parse Argument [{key}] failed!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resultStr = result.ToString().Trim().Trim('\"').Trim('\'');
|
||||||
|
|
||||||
|
// 不应该有引号出现
|
||||||
|
if (resultStr.Contains('\"') || resultStr.Contains('\'')) throw new Exception();
|
||||||
|
|
||||||
|
return resultStr;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Parse Argument [{key}] failed!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,255 +4,271 @@ 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>
|
||||||
{
|
/// See: <see cref="CommandInvoker.Input"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.Input"/>.
|
public string Input { get; set; } = default!;
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public string Input { get; set; } = default!;
|
/// See: <see cref="CommandInvoker.Headers"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.Headers"/>.
|
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
/// See: <see cref="CommandInvoker.AdKeywords"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.AdKeywords"/>.
|
public string[]? AdKeywords { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public string[]? AdKeywords { get; set; }
|
/// See: <see cref="CommandInvoker.MaxSpeed"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.MaxSpeed"/>.
|
public long? MaxSpeed { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public long? MaxSpeed { get; set; }
|
/// See: <see cref="CommandInvoker.Keys"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.Keys"/>.
|
public string[]? Keys { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public string[]? Keys { get; set; }
|
/// See: <see cref="CommandInvoker.BaseUrl"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.BaseUrl"/>.
|
public string? BaseUrl { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public string? BaseUrl { get; set; }
|
/// See: <see cref="CommandInvoker.KeyTextFile"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.KeyTextFile"/>.
|
public string? KeyTextFile { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public string? KeyTextFile { get; set; }
|
/// See: <see cref="CommandInvoker.UrlProcessorArgs"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.UrlProcessorArgs"/>.
|
public string? UrlProcessorArgs { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public string? UrlProcessorArgs { get; set; }
|
/// See: <see cref="CommandInvoker.LogLevel"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.LogLevel"/>.
|
public LogLevel LogLevel { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public LogLevel LogLevel { get; set; }
|
/// See: <see cref="CommandInvoker.NoDateInfo"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.NoDateInfo"/>.
|
public bool NoDateInfo { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool NoDateInfo { get; set; }
|
/// See: <see cref="CommandInvoker.NoLog"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.NoLog"/>.
|
public bool NoLog { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool NoLog { get; set; }
|
/// See: <see cref="CommandInvoker.AllowHlsMultiExtMap"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.AutoSelect"/>.
|
public bool AllowHlsMultiExtMap { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool AutoSelect { get; set; }
|
/// See: <see cref="CommandInvoker.AutoSelect"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.SubOnly"/>.
|
public bool AutoSelect { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool SubOnly { get; set; }
|
/// See: <see cref="CommandInvoker.DisableUpdateCheck"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.ThreadCount"/>.
|
public bool DisableUpdateCheck { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public int ThreadCount { get; set; }
|
/// See: <see cref="CommandInvoker.SubOnly"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.DownloadRetryCount"/>.
|
public bool SubOnly { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public int DownloadRetryCount { get; set; }
|
/// See: <see cref="CommandInvoker.ThreadCount"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.LiveRecordLimit"/>.
|
public int ThreadCount { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public TimeSpan? LiveRecordLimit { get; set; }
|
/// See: <see cref="CommandInvoker.DownloadRetryCount"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.TaskStartAt"/>.
|
public int DownloadRetryCount { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public DateTime? TaskStartAt { get; set; }
|
/// See: <see cref="CommandInvoker.HttpRequestTimeout"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.SkipMerge"/>.
|
public double HttpRequestTimeout { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool SkipMerge { get; set; }
|
/// See: <see cref="CommandInvoker.LiveRecordLimit"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.BinaryMerge"/>.
|
public TimeSpan? LiveRecordLimit { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool BinaryMerge { get; set; }
|
/// See: <see cref="CommandInvoker.TaskStartAt"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.ForceAnsiConsole"/>.
|
public DateTime? TaskStartAt { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool ForceAnsiConsole { get; set; }
|
/// See: <see cref="CommandInvoker.SkipMerge"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.NoAnsiColor"/>.
|
public bool SkipMerge { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool NoAnsiColor { get; set; }
|
/// See: <see cref="CommandInvoker.BinaryMerge"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.UseFFmpegConcatDemuxer"/>.
|
public bool BinaryMerge { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool UseFFmpegConcatDemuxer { get; set; }
|
/// See: <see cref="CommandInvoker.ForceAnsiConsole"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.DelAfterDone"/>.
|
public bool ForceAnsiConsole { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool DelAfterDone { get; set; }
|
/// See: <see cref="CommandInvoker.NoAnsiColor"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.AutoSubtitleFix"/>.
|
public bool NoAnsiColor { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool AutoSubtitleFix { get; set; }
|
/// See: <see cref="CommandInvoker.UseFFmpegConcatDemuxer"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.CheckSegmentsCount"/>.
|
public bool UseFFmpegConcatDemuxer { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool CheckSegmentsCount { get; set; }
|
/// See: <see cref="CommandInvoker.DelAfterDone"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.SkipDownload"/>.
|
public bool DelAfterDone { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool SkipDownload { get; set; }
|
/// See: <see cref="CommandInvoker.AutoSubtitleFix"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.WriteMetaJson"/>.
|
public bool AutoSubtitleFix { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool WriteMetaJson { get; set; }
|
/// See: <see cref="CommandInvoker.CheckSegmentsCount"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.AppendUrlParams"/>.
|
public bool CheckSegmentsCount { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool AppendUrlParams { get; set; }
|
/// See: <see cref="CommandInvoker.SkipDownload"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.MP4RealTimeDecryption"/>.
|
public bool SkipDownload { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool MP4RealTimeDecryption { get; set; }
|
/// See: <see cref="CommandInvoker.WriteMetaJson"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.UseShakaPackager"/>.
|
public bool WriteMetaJson { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool UseShakaPackager { get; set; }
|
/// See: <see cref="CommandInvoker.AppendUrlParams"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.MuxAfterDone"/>.
|
public bool AppendUrlParams { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool MuxAfterDone { get; set; }
|
/// See: <see cref="CommandInvoker.MP4RealTimeDecryption"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.ConcurrentDownload"/>.
|
public bool MP4RealTimeDecryption { get; set; }
|
||||||
/// </summary>
|
/// <summary>
|
||||||
public bool ConcurrentDownload { get; set; }
|
/// See: <see cref="CommandInvoker.UseShakaPackager"/>.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// See: <see cref="CommandInvoker.LiveRealTimeMerge"/>.
|
[Obsolete("Use DecryptionEngine instead")]
|
||||||
/// </summary>
|
public bool UseShakaPackager { get; set; }
|
||||||
public bool LiveRealTimeMerge { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.DecryptionEngine"/>.
|
||||||
/// See: <see cref="CommandInvoker.LiveKeepSegments"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public DecryptEngine DecryptionEngine { get; set; }
|
||||||
public bool LiveKeepSegments { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.MuxAfterDone"/>.
|
||||||
/// See: <see cref="CommandInvoker.LivePerformAsVod"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public bool MuxAfterDone { get; set; }
|
||||||
public bool LivePerformAsVod { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.ConcurrentDownload"/>.
|
||||||
/// See: <see cref="CommandInvoker.UseSystemProxy"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public bool ConcurrentDownload { get; set; }
|
||||||
public bool UseSystemProxy { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.LiveRealTimeMerge"/>.
|
||||||
/// See: <see cref="CommandInvoker.SubtitleFormat"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public bool LiveRealTimeMerge { get; set; }
|
||||||
public SubtitleFormat SubtitleFormat { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.LiveKeepSegments"/>.
|
||||||
/// See: <see cref="CommandInvoker.TmpDir"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public bool LiveKeepSegments { get; set; }
|
||||||
public string? TmpDir { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.LivePerformAsVod"/>.
|
||||||
/// See: <see cref="CommandInvoker.SaveDir"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public bool LivePerformAsVod { get; set; }
|
||||||
public string? SaveDir { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.UseSystemProxy"/>.
|
||||||
/// See: <see cref="CommandInvoker.SaveName"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public bool UseSystemProxy { get; set; }
|
||||||
public string? SaveName { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.SubtitleFormat"/>.
|
||||||
/// See: <see cref="CommandInvoker.SavePattern"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public SubtitleFormat SubtitleFormat { get; set; }
|
||||||
public string? SavePattern { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.TmpDir"/>.
|
||||||
/// See: <see cref="CommandInvoker.UILanguage"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? TmpDir { get; set; }
|
||||||
public string? UILanguage { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.SaveDir"/>.
|
||||||
/// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? SaveDir { get; set; }
|
||||||
public string? DecryptionBinaryPath { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.SaveName"/>.
|
||||||
/// See: <see cref="CommandInvoker.FFmpegBinaryPath"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? SaveName { get; set; }
|
||||||
public string? FFmpegBinaryPath { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.SavePattern"/>.
|
||||||
/// See: <see cref="CommandInvoker.MkvmergeBinaryPath"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? SavePattern { get; set; }
|
||||||
public string? MkvmergeBinaryPath { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.UILanguage"/>.
|
||||||
/// See: <see cref="CommandInvoker.MuxImports"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? UILanguage { get; set; }
|
||||||
public List<OutputFile>? MuxImports { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.DecryptionBinaryPath"/>.
|
||||||
/// See: <see cref="CommandInvoker.VideoFilter"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? DecryptionBinaryPath { get; set; }
|
||||||
public StreamFilter? VideoFilter { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.FFmpegBinaryPath"/>.
|
||||||
/// See: <see cref="CommandInvoker.DropVideoFilter"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? FFmpegBinaryPath { get; set; }
|
||||||
public StreamFilter? DropVideoFilter { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.MkvmergeBinaryPath"/>.
|
||||||
/// See: <see cref="CommandInvoker.AudioFilter"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public string? MkvmergeBinaryPath { get; set; }
|
||||||
public StreamFilter? AudioFilter { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.MuxImports"/>.
|
||||||
/// See: <see cref="CommandInvoker.DropAudioFilter"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public List<OutputFile>? MuxImports { get; set; }
|
||||||
public StreamFilter? DropAudioFilter { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.VideoFilter"/>.
|
||||||
/// See: <see cref="CommandInvoker.SubtitleFilter"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public StreamFilter? VideoFilter { get; set; }
|
||||||
public StreamFilter? SubtitleFilter { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.DropVideoFilter"/>.
|
||||||
/// See: <see cref="CommandInvoker.DropSubtitleFilter"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public StreamFilter? DropVideoFilter { get; set; }
|
||||||
public StreamFilter? DropSubtitleFilter { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.AudioFilter"/>.
|
||||||
/// See: <see cref="CommandInvoker.CustomHLSMethod"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public StreamFilter? AudioFilter { get; set; }
|
||||||
public EncryptMethod? CustomHLSMethod { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.DropAudioFilter"/>.
|
||||||
/// See: <see cref="CommandInvoker.CustomHLSKey"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public StreamFilter? DropAudioFilter { get; set; }
|
||||||
public byte[]? CustomHLSKey { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.SubtitleFilter"/>.
|
||||||
/// See: <see cref="CommandInvoker.CustomHLSIv"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public StreamFilter? SubtitleFilter { get; set; }
|
||||||
public byte[]? CustomHLSIv { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.DropSubtitleFilter"/>.
|
||||||
/// See: <see cref="CommandInvoker.CustomProxy"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public StreamFilter? DropSubtitleFilter { get; set; }
|
||||||
public WebProxy? CustomProxy { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.CustomHLSMethod"/>.
|
||||||
/// See: <see cref="CommandInvoker.CustomRange"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public EncryptMethod? CustomHLSMethod { get; set; }
|
||||||
public CustomRange? CustomRange { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.CustomHLSKey"/>.
|
||||||
/// See: <see cref="CommandInvoker.LiveWaitTime"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public byte[]? CustomHLSKey { get; set; }
|
||||||
public int? LiveWaitTime { get; set; }
|
/// <summary>
|
||||||
/// <summary>
|
/// See: <see cref="CommandInvoker.CustomHLSIv"/>.
|
||||||
/// See: <see cref="CommandInvoker.LiveTakeCount"/>.
|
/// </summary>
|
||||||
/// </summary>
|
public byte[]? CustomHLSIv { get; set; }
|
||||||
public int LiveTakeCount { get; set; }
|
/// <summary>
|
||||||
public MuxOptions MuxOptions { get; set; }
|
/// See: <see cref="CommandInvoker.CustomProxy"/>.
|
||||||
//public bool LiveWriteHLS { get; set; } = true;
|
/// </summary>
|
||||||
/// <summary>
|
public WebProxy? CustomProxy { get; set; }
|
||||||
/// See: <see cref="CommandInvoker.LivePipeMux"/>.
|
/// <summary>
|
||||||
/// </summary>
|
/// See: <see cref="CommandInvoker.CustomRange"/>.
|
||||||
public bool LivePipeMux { get; set; }
|
/// </summary>
|
||||||
/// <summary>
|
public CustomRange? CustomRange { get; set; }
|
||||||
/// See: <see cref="CommandInvoker.LiveFixVttByAudio"/>.
|
/// <summary>
|
||||||
/// </summary>
|
/// See: <see cref="CommandInvoker.LiveWaitTime"/>.
|
||||||
public bool LiveFixVttByAudio { get; set; }
|
/// </summary>
|
||||||
}
|
public int? LiveWaitTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// See: <see cref="CommandInvoker.LiveTakeCount"/>.
|
||||||
|
/// </summary>
|
||||||
|
public int LiveTakeCount { get; set; }
|
||||||
|
public MuxOptions? MuxOptions { get; set; }
|
||||||
|
// public bool LiveWriteHLS { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// See: <see cref="CommandInvoker.LivePipeMux"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool LivePipeMux { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// See: <see cref="CommandInvoker.LiveFixVttByAudio"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool LiveFixVttByAudio { get; set; }
|
||||||
}
|
}
|
@ -1,33 +1,25 @@
|
|||||||
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>
|
||||||
/// 前置阶段生成的文件夹名
|
/// 前置阶段生成的文件夹名
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string DirPrefix { get; set; }
|
public required string DirPrefix { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件名模板
|
/// 文件名模板
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? SavePattern { get; set; }
|
public string? SavePattern { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 校验响应头的文件大小和实际大小
|
/// 校验响应头的文件大小和实际大小
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool CheckContentLength { get; set; } = true;
|
public bool CheckContentLength { get; set; } = true;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 请求头
|
/// 请求头
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
}
|
|
@ -1,44 +1,38 @@
|
|||||||
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>
|
||||||
|
/// AES-128解密,解密后原地替换文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath"></param>
|
||||||
|
/// <param name="keyByte"></param>
|
||||||
|
/// <param name="ivByte"></param>
|
||||||
|
/// <param name="mode"></param>
|
||||||
|
/// <param name="padding"></param>
|
||||||
|
public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
|
||||||
{
|
{
|
||||||
/// <summary>
|
var fileBytes = File.ReadAllBytes(filePath);
|
||||||
/// AES-128解密,解密后原地替换文件
|
var decrypted = AES128Decrypt(fileBytes, keyByte, ivByte, mode, padding);
|
||||||
/// </summary>
|
File.WriteAllBytes(filePath, decrypted);
|
||||||
/// <param name="filePath"></param>
|
|
||||||
/// <param name="keyByte"></param>
|
|
||||||
/// <param name="ivByte"></param>
|
|
||||||
/// <param name="mode"></param>
|
|
||||||
/// <param name="padding"></param>
|
|
||||||
public static void AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
|
|
||||||
{
|
|
||||||
var fileBytes = File.ReadAllBytes(filePath);
|
|
||||||
var decrypted = AES128Decrypt(fileBytes, keyByte, ivByte, mode, padding);
|
|
||||||
File.WriteAllBytes(filePath, decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
|
|
||||||
{
|
|
||||||
byte[] inBuff = encryptedBuff;
|
|
||||||
|
|
||||||
Aes dcpt = Aes.Create();
|
|
||||||
dcpt.BlockSize = 128;
|
|
||||||
dcpt.KeySize = 128;
|
|
||||||
dcpt.Key = keyByte;
|
|
||||||
dcpt.IV = ivByte;
|
|
||||||
dcpt.Mode = mode;
|
|
||||||
dcpt.Padding = padding;
|
|
||||||
|
|
||||||
ICryptoTransform cTransform = dcpt.CreateDecryptor();
|
|
||||||
byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length);
|
|
||||||
return resultArray;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7)
|
||||||
|
{
|
||||||
|
byte[] inBuff = encryptedBuff;
|
||||||
|
|
||||||
|
Aes dcpt = Aes.Create();
|
||||||
|
dcpt.BlockSize = 128;
|
||||||
|
dcpt.KeySize = 128;
|
||||||
|
dcpt.Key = keyByte;
|
||||||
|
dcpt.IV = ivByte;
|
||||||
|
dcpt.Mode = mode;
|
||||||
|
dcpt.Padding = padding;
|
||||||
|
|
||||||
|
ICryptoTransform cTransform = dcpt.CreateDecryptor();
|
||||||
|
byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length);
|
||||||
|
return resultArray;
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,21 @@
|
|||||||
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)
|
||||||
{
|
throw new Exception("Key must be 32 bytes!");
|
||||||
if (keyBytes.Length != 32)
|
if (nonceBytes.Length != 12 && nonceBytes.Length != 8)
|
||||||
throw new Exception("Key must be 32 bytes!");
|
throw new Exception("Key must be 12 or 8 bytes!");
|
||||||
if (nonceBytes.Length != 12 && nonceBytes.Length != 8)
|
if (nonceBytes.Length == 8)
|
||||||
throw new Exception("Key must be 12 or 8 bytes!");
|
nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray();
|
||||||
if (nonceBytes.Length == 8)
|
|
||||||
nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray();
|
|
||||||
|
|
||||||
var decStream = new MemoryStream();
|
var decStream = new MemoryStream();
|
||||||
using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff));
|
using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff));
|
||||||
using (BinaryWriter writer = new BinaryWriter(decStream))
|
using (BinaryWriter writer = new BinaryWriter(decStream))
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var buffer = reader.ReadBytes(1024);
|
var buffer = reader.ReadBytes(1024);
|
||||||
@ -37,7 +32,6 @@ namespace N_m3u8DL_RE.Crypto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return decStream.ToArray();
|
return decStream.ToArray();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,19 +5,19 @@
|
|||||||
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
|
<IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
|
||||||
<StaticallyLinked Condition="$(RuntimeIdentifier.StartsWith('win'))">true</StaticallyLinked>
|
<StaticallyLinked Condition="$(RuntimeIdentifier.StartsWith('win'))">true</StaticallyLinked>
|
||||||
<TrimMode>full</TrimMode>
|
<TrimMode>full</TrimMode>
|
||||||
<TrimmerDefaultAction>link</TrimmerDefaultAction>
|
<TrimmerDefaultAction>link</TrimmerDefaultAction>
|
||||||
<IlcTrimMetadata>true</IlcTrimMetadata>
|
<IlcTrimMetadata>true</IlcTrimMetadata>
|
||||||
<IlcGenerateStackTraceData>true</IlcGenerateStackTraceData>
|
<IlcGenerateStackTraceData>true</IlcGenerateStackTraceData>
|
||||||
<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" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -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,244 +9,234 @@ 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;
|
||||||
|
DownloaderConfig DownloaderConfig;
|
||||||
|
StreamExtractor StreamExtractor;
|
||||||
|
List<StreamSpec> SelectedSteams;
|
||||||
|
List<OutputFile> OutputFiles = [];
|
||||||
|
DateTime NowDateTime;
|
||||||
|
DateTime? PublishDateTime;
|
||||||
|
bool STOP_FLAG = false;
|
||||||
|
bool READ_IFO = false;
|
||||||
|
ConcurrentDictionary<int, int> RecordingDurDic = new(); // 已录制时长
|
||||||
|
ConcurrentDictionary<int, double> RecordingSizeDic = new(); // 已录制大小
|
||||||
|
CancellationTokenSource CancellationTokenSource = new(); // 取消Wait
|
||||||
|
List<byte> InfoBuffer = new List<byte>(188 * 5000); // 5000个分包中解析信息,没有就算了
|
||||||
|
|
||||||
|
public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
|
||||||
{
|
{
|
||||||
IDownloader Downloader;
|
this.DownloaderConfig = downloaderConfig;
|
||||||
DownloaderConfig DownloaderConfig;
|
Downloader = new SimpleDownloader(DownloaderConfig);
|
||||||
StreamExtractor StreamExtractor;
|
NowDateTime = DateTime.Now;
|
||||||
List<StreamSpec> SelectedSteams;
|
PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;
|
||||||
List<OutputFile> OutputFiles = new();
|
StreamExtractor = streamExtractor;
|
||||||
DateTime NowDateTime;
|
SelectedSteams = selectedSteams;
|
||||||
DateTime? PublishDateTime;
|
}
|
||||||
bool STOP_FLAG = false;
|
|
||||||
bool READ_IFO = false;
|
|
||||||
ConcurrentDictionary<int, int> RecordingDurDic = new(); //已录制时长
|
|
||||||
ConcurrentDictionary<int, double> RecordingSizeDic = new(); //已录制大小
|
|
||||||
CancellationTokenSource CancellationTokenSource = new(); //取消Wait
|
|
||||||
List<byte> InfoBuffer = new List<byte>(188 * 5000); //5000个分包中解析信息,没有就算了
|
|
||||||
|
|
||||||
public HTTPLiveRecordManager(DownloaderConfig downloaderConfig, List<StreamSpec> selectedSteams, StreamExtractor streamExtractor)
|
private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)
|
||||||
|
{
|
||||||
|
task.MaxValue = 1;
|
||||||
|
task.StartTask();
|
||||||
|
|
||||||
|
var name = streamSpec.ToShortString();
|
||||||
|
var dirName = $"{DownloaderConfig.MyOptions.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}";
|
||||||
|
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
|
||||||
|
var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName;
|
||||||
|
|
||||||
|
Logger.Debug($"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}");
|
||||||
|
|
||||||
|
// 创建文件夹
|
||||||
|
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url));
|
||||||
|
request.Headers.ConnectionClose = false;
|
||||||
|
foreach (var item in DownloaderConfig.Headers)
|
||||||
{
|
{
|
||||||
this.DownloaderConfig = downloaderConfig;
|
request.Headers.TryAddWithoutValidation(item.Key, item.Value);
|
||||||
Downloader = new SimpleDownloader(DownloaderConfig);
|
}
|
||||||
NowDateTime = DateTime.Now;
|
Logger.Debug(request.Headers.ToString());
|
||||||
PublishDateTime = selectedSteams.FirstOrDefault()?.PublishTime;
|
|
||||||
StreamExtractor = streamExtractor;
|
using var response = await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationTokenSource.Token);
|
||||||
SelectedSteams = selectedSteams;
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var output = Path.Combine(saveDir, saveName + ".ts");
|
||||||
|
using var stream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
using var responseStream = await response.Content.ReadAsStreamAsync(CancellationTokenSource.Token);
|
||||||
|
var buffer = new byte[16 * 1024];
|
||||||
|
var size = 0;
|
||||||
|
|
||||||
|
// 计时器
|
||||||
|
_ = TimeCounterAsync();
|
||||||
|
// 读取INFO
|
||||||
|
_ = ReadInfoAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while ((size = await responseStream.ReadAsync(buffer, CancellationTokenSource.Token)) > 0)
|
||||||
|
{
|
||||||
|
if (!READ_IFO && InfoBuffer.Count < 188 * 5000)
|
||||||
|
{
|
||||||
|
InfoBuffer.AddRange(buffer);
|
||||||
|
}
|
||||||
|
speedContainer.Add(size);
|
||||||
|
RecordingSizeDic[task.Id] += size;
|
||||||
|
await stream.WriteAsync(buffer, 0, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
|
||||||
|
{
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> RecordStreamAsync(StreamSpec streamSpec, ProgressTask task, SpeedContainer speedContainer)
|
Logger.InfoMarkUp("File Size: " + GlobalUtil.FormatFileSize(RecordingSizeDic[task.Id]));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReadInfoAsync()
|
||||||
|
{
|
||||||
|
while (!STOP_FLAG && !READ_IFO)
|
||||||
{
|
{
|
||||||
task.MaxValue = 1;
|
await Task.Delay(200);
|
||||||
task.StartTask();
|
if (InfoBuffer.Count < 188 * 5000) continue;
|
||||||
|
|
||||||
var name = streamSpec.ToShortString();
|
ushort ConvertToUint16(IEnumerable<byte> bytes)
|
||||||
var dirName = $"{DownloaderConfig.MyOptions.SaveName ?? NowDateTime.ToString("yyyy-MM-dd_HH-mm-ss")}_{task.Id}_{OtherUtil.GetValidFileName(streamSpec.GroupId ?? "", "-")}_{streamSpec.Codecs}_{streamSpec.Bandwidth}_{streamSpec.Language}";
|
|
||||||
var saveDir = DownloaderConfig.MyOptions.SaveDir ?? Environment.CurrentDirectory;
|
|
||||||
var saveName = DownloaderConfig.MyOptions.SaveName != null ? $"{DownloaderConfig.MyOptions.SaveName}.{streamSpec.Language}".TrimEnd('.') : dirName;
|
|
||||||
|
|
||||||
Logger.Debug($"dirName: {dirName}; saveDir: {saveDir}; saveName: {saveName}");
|
|
||||||
|
|
||||||
//创建文件夹
|
|
||||||
if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir);
|
|
||||||
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(streamSpec.Url));
|
|
||||||
request.Headers.ConnectionClose = false;
|
|
||||||
foreach (var item in DownloaderConfig.Headers)
|
|
||||||
{
|
{
|
||||||
request.Headers.TryAddWithoutValidation(item.Key, item.Value);
|
if (BitConverter.IsLittleEndian)
|
||||||
|
bytes = bytes.Reverse();
|
||||||
|
return BitConverter.ToUInt16(bytes.ToArray());
|
||||||
}
|
}
|
||||||
Logger.Debug(request.Headers.ToString());
|
|
||||||
|
|
||||||
using var response = await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationTokenSource.Token);
|
var data = InfoBuffer.ToArray();
|
||||||
response.EnsureSuccessStatusCode();
|
var programId = "";
|
||||||
|
var serviceProvider = "";
|
||||||
var output = Path.Combine(saveDir, saveName + ".ts");
|
var serviceName = "";
|
||||||
using var stream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
for (int i = 0; i < data.Length; i++)
|
||||||
using var responseStream = await response.Content.ReadAsStreamAsync(CancellationTokenSource.Token);
|
|
||||||
var buffer = new byte[16 * 1024];
|
|
||||||
var size = 0;
|
|
||||||
|
|
||||||
//计时器
|
|
||||||
TimeCounterAsync();
|
|
||||||
//读取INFO
|
|
||||||
ReadInfoAsync();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
while ((size = await responseStream.ReadAsync(buffer, CancellationTokenSource.Token)) > 0)
|
if (data[i] == 0x47 && (i + 188) < data.Length && data[i + 188] == 0x47)
|
||||||
{
|
{
|
||||||
if (!READ_IFO && InfoBuffer.Count < 188 * 5000)
|
var tsData = data.Skip(i).Take(188);
|
||||||
|
var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0);
|
||||||
|
var pid = (tsHeaderInt & 0x1fff00) >> 8;
|
||||||
|
var tsPayload = tsData.Skip(4);
|
||||||
|
// PAT
|
||||||
|
if (pid == 0x0000)
|
||||||
{
|
{
|
||||||
InfoBuffer.AddRange(buffer);
|
programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString();
|
||||||
}
|
}
|
||||||
speedContainer.Add(size);
|
// SDT, BAT, ST
|
||||||
RecordingSizeDic[task.Id] += size;
|
else if (pid == 0x0011)
|
||||||
await stream.WriteAsync(buffer, 0, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException oce) when (oce.CancellationToken == CancellationTokenSource.Token)
|
|
||||||
{
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.InfoMarkUp("File Size: " + GlobalUtil.FormatFileSize(RecordingSizeDic[task.Id]));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ReadInfoAsync()
|
|
||||||
{
|
|
||||||
while (!STOP_FLAG && !READ_IFO)
|
|
||||||
{
|
|
||||||
await Task.Delay(200);
|
|
||||||
if (InfoBuffer.Count < 188 * 5000) continue;
|
|
||||||
|
|
||||||
UInt16 ConvertToUint16(IEnumerable<byte> bytes)
|
|
||||||
{
|
|
||||||
if (BitConverter.IsLittleEndian)
|
|
||||||
bytes = bytes.Reverse();
|
|
||||||
return BitConverter.ToUInt16(bytes.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = InfoBuffer.ToArray();
|
|
||||||
var programId = "";
|
|
||||||
var serviceProvider = "";
|
|
||||||
var serviceName = "";
|
|
||||||
for (int i = 0; i < data.Length; i++)
|
|
||||||
{
|
|
||||||
if (data[i] == 0x47 && (i + 188) < data.Length && data[i + 188] == 0x47)
|
|
||||||
{
|
{
|
||||||
var tsData = data.Skip(i).Take(188);
|
var tableId = (int)tsPayload.Skip(1).First();
|
||||||
var tsHeaderInt = BitConverter.ToUInt32(BitConverter.IsLittleEndian ? tsData.Take(4).Reverse().ToArray() : tsData.Take(4).ToArray(), 0);
|
// Current TS Info
|
||||||
var pid = (tsHeaderInt & 0x1fff00) >> 8;
|
if (tableId == 0x42)
|
||||||
var tsPayload = tsData.Skip(4);
|
|
||||||
//PAT
|
|
||||||
if (pid == 0x0000)
|
|
||||||
{
|
{
|
||||||
programId = ConvertToUint16(tsPayload.Skip(9).Take(2)).ToString();
|
var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff;
|
||||||
|
var sectionData = tsPayload.Skip(4).Take(sectionLength);
|
||||||
|
var dscripData = sectionData.Skip(8);
|
||||||
|
var descriptorsLoopLength = (ConvertToUint16(dscripData.Skip(3).Take(2))) & 0xfff;
|
||||||
|
var descriptorsData = dscripData.Skip(5).Take(descriptorsLoopLength);
|
||||||
|
var serviceProviderLength = (int)descriptorsData.Skip(3).First();
|
||||||
|
serviceProvider = Encoding.UTF8.GetString(descriptorsData.Skip(4).Take(serviceProviderLength).ToArray());
|
||||||
|
var serviceNameLength = (int)descriptorsData.Skip(4 + serviceProviderLength).First();
|
||||||
|
serviceName = Encoding.UTF8.GetString(descriptorsData.Skip(5 + serviceProviderLength).Take(serviceNameLength).ToArray());
|
||||||
}
|
}
|
||||||
//SDT, BAT, ST
|
|
||||||
else if (pid == 0x0011)
|
|
||||||
{
|
|
||||||
var tableId = (int)tsPayload.Skip(1).First();
|
|
||||||
//Current TS Info
|
|
||||||
if (tableId == 0x42)
|
|
||||||
{
|
|
||||||
var sectionLength = ConvertToUint16(tsPayload.Skip(2).Take(2)) & 0xfff;
|
|
||||||
var sectionData = tsPayload.Skip(4).Take(sectionLength);
|
|
||||||
var dscripData = sectionData.Skip(8);
|
|
||||||
var descriptorsLoopLength = (ConvertToUint16(dscripData.Skip(3).Take(2))) & 0xfff;
|
|
||||||
var descriptorsData = dscripData.Skip(5).Take(descriptorsLoopLength);
|
|
||||||
var serviceProviderLength = (int)descriptorsData.Skip(3).First();
|
|
||||||
serviceProvider = Encoding.UTF8.GetString(descriptorsData.Skip(4).Take(serviceProviderLength).ToArray());
|
|
||||||
var serviceNameLength = (int)descriptorsData.Skip(4 + serviceProviderLength).First();
|
|
||||||
serviceName = Encoding.UTF8.GetString(descriptorsData.Skip(5 + serviceProviderLength).Take(serviceNameLength).ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (programId != "" && (serviceName != "" || serviceProvider != ""))
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
if (programId != "" && (serviceName != "" || serviceProvider != ""))
|
||||||
|
break;
|
||||||
if (!string.IsNullOrEmpty(programId))
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp($"Program Id: [cyan]{programId.EscapeMarkup()}[/]");
|
|
||||||
if (!string.IsNullOrEmpty(serviceName)) Logger.InfoMarkUp($"Service Name: [cyan]{serviceName.EscapeMarkup()}[/]");
|
|
||||||
if (!string.IsNullOrEmpty(serviceProvider)) Logger.InfoMarkUp($"Service Provider: [cyan]{serviceProvider.EscapeMarkup()}[/]");
|
|
||||||
READ_IFO = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TimeCounterAsync()
|
if (!string.IsNullOrEmpty(programId))
|
||||||
{
|
|
||||||
while (!STOP_FLAG)
|
|
||||||
{
|
{
|
||||||
await Task.Delay(1000);
|
Logger.InfoMarkUp($"Program Id: [cyan]{programId.EscapeMarkup()}[/]");
|
||||||
RecordingDurDic[0]++;
|
if (!string.IsNullOrEmpty(serviceName)) Logger.InfoMarkUp($"Service Name: [cyan]{serviceName.EscapeMarkup()}[/]");
|
||||||
|
if (!string.IsNullOrEmpty(serviceProvider)) Logger.InfoMarkUp($"Service Provider: [cyan]{serviceProvider.EscapeMarkup()}[/]");
|
||||||
//检测时长限制
|
READ_IFO = true;
|
||||||
if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds))
|
|
||||||
{
|
|
||||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
|
|
||||||
STOP_FLAG = true;
|
|
||||||
CancellationTokenSource.Cancel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> StartRecordAsync()
|
|
||||||
{
|
|
||||||
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); //速度计算
|
|
||||||
ConcurrentDictionary<StreamSpec, bool?> Results = new();
|
|
||||||
|
|
||||||
var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
|
|
||||||
progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;
|
|
||||||
|
|
||||||
//进度条的列定义
|
|
||||||
var progressColumns = new ProgressColumn[]
|
|
||||||
{
|
|
||||||
new TaskDescriptionColumn() { Alignment = Justify.Left },
|
|
||||||
new RecordingDurationColumn(RecordingDurDic), //时长显示
|
|
||||||
new RecordingSizeColumn(RecordingSizeDic), //大小显示
|
|
||||||
new RecordingStatusColumn(),
|
|
||||||
new DownloadSpeedColumn(SpeedContainerDic), //速度计算
|
|
||||||
new SpinnerColumn(),
|
|
||||||
};
|
|
||||||
if (DownloaderConfig.MyOptions.NoAnsiColor)
|
|
||||||
{
|
|
||||||
progressColumns = progressColumns.SkipLast(1).ToArray();
|
|
||||||
}
|
|
||||||
progress.Columns(progressColumns);
|
|
||||||
|
|
||||||
await progress.StartAsync(async ctx =>
|
|
||||||
{
|
|
||||||
//创建任务
|
|
||||||
var dic = SelectedSteams.Select(item =>
|
|
||||||
{
|
|
||||||
var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0);
|
|
||||||
SpeedContainerDic[task.Id] = new SpeedContainer(); //速度计算
|
|
||||||
RecordingDurDic[task.Id] = 0;
|
|
||||||
RecordingSizeDic[task.Id] = 0;
|
|
||||||
return (item, task);
|
|
||||||
}).ToDictionary(item => item.item, item => item.task);
|
|
||||||
|
|
||||||
DownloaderConfig.MyOptions.LiveRecordLimit = DownloaderConfig.MyOptions.LiveRecordLimit ?? TimeSpan.MaxValue;
|
|
||||||
var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
|
|
||||||
if (limit != TimeSpan.MaxValue)
|
|
||||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");
|
|
||||||
//录制直播时,用户选了几个流就并发录几个
|
|
||||||
var options = new ParallelOptions()
|
|
||||||
{
|
|
||||||
MaxDegreeOfParallelism = SelectedSteams.Count
|
|
||||||
};
|
|
||||||
//并发下载
|
|
||||||
await Parallel.ForEachAsync(dic, options, async (kp, _) =>
|
|
||||||
{
|
|
||||||
var task = kp.Value;
|
|
||||||
var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);
|
|
||||||
Results[kp.Key] = await consumerTask;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var success = Results.Values.All(v => v == true);
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public async Task TimeCounterAsync()
|
||||||
|
{
|
||||||
|
while (!STOP_FLAG)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
RecordingDurDic[0]++;
|
||||||
|
|
||||||
|
// 检测时长限制
|
||||||
|
if (RecordingDurDic.All(d => d.Value >= DownloaderConfig.MyOptions.LiveRecordLimit?.TotalSeconds))
|
||||||
|
{
|
||||||
|
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimitReached}[/]");
|
||||||
|
STOP_FLAG = true;
|
||||||
|
CancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> StartRecordAsync()
|
||||||
|
{
|
||||||
|
ConcurrentDictionary<int, SpeedContainer> SpeedContainerDic = new(); // 速度计算
|
||||||
|
ConcurrentDictionary<StreamSpec, bool?> Results = new();
|
||||||
|
|
||||||
|
var progress = CustomAnsiConsole.Console.Progress().AutoClear(true);
|
||||||
|
progress.AutoRefresh = DownloaderConfig.MyOptions.LogLevel != LogLevel.OFF;
|
||||||
|
|
||||||
|
// 进度条的列定义
|
||||||
|
var progressColumns = new ProgressColumn[]
|
||||||
|
{
|
||||||
|
new TaskDescriptionColumn() { Alignment = Justify.Left },
|
||||||
|
new RecordingDurationColumn(RecordingDurDic), // 时长显示
|
||||||
|
new RecordingSizeColumn(RecordingSizeDic), // 大小显示
|
||||||
|
new RecordingStatusColumn(),
|
||||||
|
new DownloadSpeedColumn(SpeedContainerDic), // 速度计算
|
||||||
|
new SpinnerColumn(),
|
||||||
|
};
|
||||||
|
if (DownloaderConfig.MyOptions.NoAnsiColor)
|
||||||
|
{
|
||||||
|
progressColumns = progressColumns.SkipLast(1).ToArray();
|
||||||
|
}
|
||||||
|
progress.Columns(progressColumns);
|
||||||
|
|
||||||
|
await progress.StartAsync(async ctx =>
|
||||||
|
{
|
||||||
|
// 创建任务
|
||||||
|
var dic = SelectedSteams.Select(item =>
|
||||||
|
{
|
||||||
|
var task = ctx.AddTask(item.ToShortString(), autoStart: false, maxValue: 0);
|
||||||
|
SpeedContainerDic[task.Id] = new SpeedContainer(); // 速度计算
|
||||||
|
RecordingDurDic[task.Id] = 0;
|
||||||
|
RecordingSizeDic[task.Id] = 0;
|
||||||
|
return (item, task);
|
||||||
|
}).ToDictionary(item => item.item, item => item.task);
|
||||||
|
|
||||||
|
DownloaderConfig.MyOptions.LiveRecordLimit ??= TimeSpan.MaxValue;
|
||||||
|
var limit = DownloaderConfig.MyOptions.LiveRecordLimit;
|
||||||
|
if (limit != TimeSpan.MaxValue)
|
||||||
|
Logger.WarnMarkUp($"[darkorange3_1]{ResString.liveLimit}{GlobalUtil.FormatTime((int)limit.Value.TotalSeconds)}[/]");
|
||||||
|
// 录制直播时,用户选了几个流就并发录几个
|
||||||
|
var options = new ParallelOptions()
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = SelectedSteams.Count
|
||||||
|
};
|
||||||
|
// 并发下载
|
||||||
|
await Parallel.ForEachAsync(dic, options, async (kp, _) =>
|
||||||
|
{
|
||||||
|
var task = kp.Value;
|
||||||
|
var consumerTask = RecordStreamAsync(kp.Key, task, SpeedContainerDic[task.Id]);
|
||||||
|
Results[kp.Key] = await consumerTask;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var success = Results.Values.All(v => v == true);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,156 +6,149 @@ 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>
|
DownloaderConfig DownloaderConfig;
|
||||||
/// 简单下载器
|
|
||||||
/// </summary>
|
public SimpleDownloader(DownloaderConfig config)
|
||||||
internal class SimpleDownloader : IDownloader
|
|
||||||
{
|
{
|
||||||
DownloaderConfig DownloaderConfig;
|
DownloaderConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
public SimpleDownloader(DownloaderConfig config)
|
public async Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null)
|
||||||
|
{
|
||||||
|
var url = segment.Url;
|
||||||
|
var (des, dResult) = await DownClipAsync(url, savePath, speedContainer, segment.StartRange, segment.StopRange, headers, DownloaderConfig.MyOptions.DownloadRetryCount);
|
||||||
|
if (dResult is { Success: true } && dResult.ActualFilePath != des)
|
||||||
{
|
{
|
||||||
DownloaderConfig = config;
|
switch (segment.EncryptInfo.Method)
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DownloadResult?> DownloadSegmentAsync(MediaSegment segment, string savePath, SpeedContainer speedContainer, Dictionary<string, string>? headers = null)
|
|
||||||
{
|
|
||||||
var url = segment.Url;
|
|
||||||
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 (segment.EncryptInfo != null)
|
case EncryptMethod.AES_128:
|
||||||
{
|
{
|
||||||
if (segment.EncryptInfo.Method == EncryptMethod.AES_128)
|
var key = segment.EncryptInfo.Key;
|
||||||
{
|
var iv = segment.EncryptInfo.IV;
|
||||||
var key = segment.EncryptInfo.Key;
|
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!);
|
||||||
var iv = segment.EncryptInfo.IV;
|
break;
|
||||||
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!);
|
|
||||||
}
|
|
||||||
else if (segment.EncryptInfo.Method == EncryptMethod.AES_128_ECB)
|
|
||||||
{
|
|
||||||
var key = segment.EncryptInfo.Key;
|
|
||||||
var iv = segment.EncryptInfo.IV;
|
|
||||||
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB);
|
|
||||||
}
|
|
||||||
else if (segment.EncryptInfo.Method == EncryptMethod.CHACHA20)
|
|
||||||
{
|
|
||||||
var key = segment.EncryptInfo.Key;
|
|
||||||
var nonce = segment.EncryptInfo.IV;
|
|
||||||
|
|
||||||
var fileBytes = File.ReadAllBytes(dResult.ActualFilePath);
|
|
||||||
var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!);
|
|
||||||
await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted);
|
|
||||||
}
|
|
||||||
else if (segment.EncryptInfo.Method == EncryptMethod.SAMPLE_AES_CTR)
|
|
||||||
{
|
|
||||||
//throw new NotSupportedException("SAMPLE-AES-CTR");
|
|
||||||
}
|
|
||||||
|
|
||||||
//Image头处理
|
|
||||||
if (dResult.ImageHeader)
|
|
||||||
{
|
|
||||||
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
|
|
||||||
}
|
|
||||||
//Gzip解压
|
|
||||||
if (dResult.GzipHeader)
|
|
||||||
{
|
|
||||||
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
case EncryptMethod.AES_128_ECB:
|
||||||
|
{
|
||||||
|
var key = segment.EncryptInfo.Key;
|
||||||
|
var iv = segment.EncryptInfo.IV;
|
||||||
|
AESUtil.AES128Decrypt(dResult.ActualFilePath, key!, iv!, System.Security.Cryptography.CipherMode.ECB);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EncryptMethod.CHACHA20:
|
||||||
|
{
|
||||||
|
var key = segment.EncryptInfo.Key;
|
||||||
|
var nonce = segment.EncryptInfo.IV;
|
||||||
|
|
||||||
//处理完成后改名
|
var fileBytes = File.ReadAllBytes(dResult.ActualFilePath);
|
||||||
File.Move(dResult.ActualFilePath, des);
|
var decrypted = ChaCha20Util.DecryptPer1024Bytes(fileBytes, key!, nonce!);
|
||||||
dResult.ActualFilePath = des;
|
await File.WriteAllBytesAsync(dResult.ActualFilePath, decrypted);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EncryptMethod.SAMPLE_AES_CTR:
|
||||||
|
// throw new NotSupportedException("SAMPLE-AES-CTR");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return dResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(string des, DownloadResult? dResult)> DownClipAsync(string url, string path, SpeedContainer speedContainer, long? fromPosition, long? toPosition, Dictionary<string, string>? headers = null, int retryCount = 3)
|
// Image头处理
|
||||||
{
|
if (dResult.ImageHeader)
|
||||||
CancellationTokenSource? cancellationTokenSource = null;
|
{
|
||||||
|
await ImageHeaderUtil.ProcessAsync(dResult.ActualFilePath);
|
||||||
|
}
|
||||||
|
// Gzip解压
|
||||||
|
if (dResult.GzipHeader)
|
||||||
|
{
|
||||||
|
await OtherUtil.DeGzipFileAsync(dResult.ActualFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理完成后改名
|
||||||
|
File.Move(dResult.ActualFilePath, des);
|
||||||
|
dResult.ActualFilePath = des;
|
||||||
|
}
|
||||||
|
return dResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string des, DownloadResult? dResult)> DownClipAsync(string url, string path, SpeedContainer speedContainer, long? fromPosition, long? toPosition, Dictionary<string, string>? headers = null, int retryCount = 3)
|
||||||
|
{
|
||||||
|
CancellationTokenSource? cancellationTokenSource = null;
|
||||||
retry:
|
retry:
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
cancellationTokenSource = new();
|
||||||
|
var des = Path.ChangeExtension(path, null);
|
||||||
|
|
||||||
|
// 已下载跳过
|
||||||
|
if (File.Exists(des))
|
||||||
{
|
{
|
||||||
cancellationTokenSource = new();
|
speedContainer.Add(new FileInfo(des).Length);
|
||||||
var des = Path.ChangeExtension(path, null);
|
return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des });
|
||||||
|
}
|
||||||
|
|
||||||
//已下载跳过
|
// 已解密跳过
|
||||||
if (File.Exists(des))
|
var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + "_dec" + Path.GetExtension(des));
|
||||||
{
|
if (File.Exists(dec))
|
||||||
speedContainer.Add(new FileInfo(des).Length);
|
{
|
||||||
return (des, new DownloadResult() { ActualContentLength = 0, ActualFilePath = des });
|
speedContainer.Add(new FileInfo(dec).Length);
|
||||||
}
|
return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec });
|
||||||
|
}
|
||||||
|
|
||||||
//已解密跳过
|
// 另起线程进行监控
|
||||||
var dec = Path.Combine(Path.GetDirectoryName(des)!, Path.GetFileNameWithoutExtension(des) + "_dec" + Path.GetExtension(des));
|
var cts = cancellationTokenSource;
|
||||||
if (File.Exists(dec))
|
using var watcher = Task.Factory.StartNew(async () =>
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
speedContainer.Add(new FileInfo(dec).Length);
|
if (cts.IsCancellationRequested) break;
|
||||||
return (dec, new DownloadResult() { ActualContentLength = 0, ActualFilePath = dec });
|
if (speedContainer.ShouldStop)
|
||||||
}
|
|
||||||
|
|
||||||
//另起线程进行监控
|
|
||||||
using var watcher = Task.Factory.StartNew(async () =>
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
if (cancellationTokenSource == null || cancellationTokenSource.IsCancellationRequested) break;
|
cts.Cancel();
|
||||||
if (speedContainer.ShouldStop)
|
Logger.DebugMarkUp("Cancel...");
|
||||||
{
|
break;
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
Logger.DebugMarkUp("Cancel...");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await Task.Delay(500);
|
|
||||||
}
|
}
|
||||||
});
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//调用下载
|
// 调用下载
|
||||||
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);
|
||||||
|
|
||||||
throw new Exception("please retry");
|
throw new Exception("please retry");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
|
||||||
|
Logger.Debug(url + " " + ex);
|
||||||
|
Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
|
||||||
|
if (retryCount-- > 0)
|
||||||
{
|
{
|
||||||
Logger.DebugMarkUp($"[grey]{ex.Message.EscapeMarkup()} retryCount: {retryCount}[/]");
|
await Task.Delay(1000);
|
||||||
Logger.Debug(url + " " + ex.ToString());
|
goto retry;
|
||||||
Logger.Extra($"Ah oh!{Environment.NewLine}RetryCount => {retryCount}{Environment.NewLine}Exception => {ex.Message}{Environment.NewLine}Url => {url}");
|
|
||||||
if (retryCount-- > 0)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
goto retry;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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()}[/]");
|
|
||||||
}
|
|
||||||
//throw new Exception("download failed", ex);
|
|
||||||
return default;
|
|
||||||
}
|
}
|
||||||
finally
|
else
|
||||||
{
|
{
|
||||||
if (cancellationTokenSource != null)
|
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()}[/]");
|
||||||
//调用后销毁
|
}
|
||||||
cancellationTokenSource.Dispose();
|
// throw new Exception("download failed", ex);
|
||||||
cancellationTokenSource = null;
|
return default;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (cancellationTokenSource != null)
|
||||||
|
{
|
||||||
|
// 调用后销毁
|
||||||
|
cancellationTokenSource.Dispose();
|
||||||
|
cancellationTokenSource = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,23 +1,16 @@
|
|||||||
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 double? StartSec { get; set; }
|
||||||
|
public double? EndSec { get; set; }
|
||||||
|
|
||||||
|
public long? StartSegIndex { get; set; }
|
||||||
|
public long? EndSegIndex { get; set;}
|
||||||
|
|
||||||
|
public override string? ToString()
|
||||||
{
|
{
|
||||||
public required string InputStr { get; set; }
|
return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}";
|
||||||
public double? StartSec { get; set; }
|
|
||||||
public double? EndSec { get; set; }
|
|
||||||
|
|
||||||
public long? StartSegIndex { get; set; }
|
|
||||||
public long? EndSegIndex { get; set;}
|
|
||||||
|
|
||||||
public override string? ToString()
|
|
||||||
{
|
|
||||||
return $"StartSec: {StartSec}, EndSec: {EndSec}, StartSegIndex: {StartSegIndex}, EndSegIndex: {EndSegIndex}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 long? RespContentLength { get; set; }
|
||||||
public bool Success { get => (ActualContentLength != null && RespContentLength != null) ? (RespContentLength == ActualContentLength) : (ActualContentLength == null ? false : true); }
|
public long? ActualContentLength { get; set; }
|
||||||
public long? RespContentLength { get; set; }
|
public bool ImageHeader { get; set; } = false; // 图片伪装
|
||||||
public long? ActualContentLength { get; set; }
|
public bool GzipHeader { get; set; } = false; // GZip压缩
|
||||||
public bool ImageHeader { get; set; } = false; //图片伪装
|
public required string ActualFilePath { get; set; }
|
||||||
public bool GzipHeader { get; set; } = false; //GZip压缩
|
}
|
||||||
public required string ActualFilePath { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +1,27 @@
|
|||||||
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? Text { get; set; }
|
||||||
|
public string? BaseInfo { get; set; }
|
||||||
|
public string? Bitrate { get; set; }
|
||||||
|
public string? Resolution { get; set; }
|
||||||
|
public string? Fps { get; set; }
|
||||||
|
public string? Type { get; set; }
|
||||||
|
public TimeSpan? StartTime { get; set; }
|
||||||
|
public bool DolbyVison { get; set; }
|
||||||
|
public bool HDR { get; set; }
|
||||||
|
|
||||||
|
public override string? ToString()
|
||||||
{
|
{
|
||||||
public string? Id { get; set; }
|
return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List<string?> { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i)));
|
||||||
public string? Text { get; set; }
|
|
||||||
public string? BaseInfo { get; set; }
|
|
||||||
public string? Bitrate { get; set; }
|
|
||||||
public string? Resolution { get; set; }
|
|
||||||
public string? Fps { get; set; }
|
|
||||||
public string? Type { get; set; }
|
|
||||||
public TimeSpan? StartTime { get; set; }
|
|
||||||
public bool DolbyVison { get; set; }
|
|
||||||
public bool HDR { get; set; }
|
|
||||||
|
|
||||||
public override string? ToString()
|
|
||||||
{
|
|
||||||
return $"{(string.IsNullOrEmpty(Id) ? "NaN" : Id)}: " + string.Join(", ", new List<string?> { Type, BaseInfo, Resolution, Fps, Bitrate }.Where(i => !string.IsNullOrEmpty(i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToStringMarkUp()
|
|
||||||
{
|
|
||||||
return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public string ToStringMarkUp()
|
||||||
|
{
|
||||||
|
return "[steelblue]" + ToString().EscapeMarkup() + ((HDR && !DolbyVison) ? " [darkorange3_1][[HDR]][/]" : "") + (DolbyVison ? " [darkorange3_1][[DOVI]][/]" : "") + "[/]";
|
||||||
|
}
|
||||||
|
}
|
@ -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 MuxFormat MuxFormat { get; set; } = MuxFormat.MP4;
|
||||||
public bool UseMkvmerge { get; set; } = false;
|
public bool KeepFiles { get; set; } = false;
|
||||||
public bool MuxToMp4 { get; set; } = false;
|
public bool SkipSubtitle { get; set; } = false;
|
||||||
public bool KeepFiles { get; set; } = false;
|
public string? BinPath { get; set; }
|
||||||
public bool SkipSubtitle { get; set; } = false;
|
}
|
||||||
public string? BinPath { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 required int Index { get; set; }
|
||||||
public MediaType? MediaType { get; set; }
|
public required string FilePath { get; set; }
|
||||||
public required int Index { get; set; }
|
public string? LangCode { get; set; }
|
||||||
public required string FilePath { get; set; }
|
public string? Description { get; set; }
|
||||||
public string? LangCode { get; set; }
|
public List<Mediainfo> Mediainfos { get; set; } = [];
|
||||||
public string? Description { get; set; }
|
}
|
||||||
public List<Mediainfo> Mediainfos { get; set; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +1,49 @@
|
|||||||
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 long NowSpeed { get; set; } = 0L; // 当前每秒速度
|
||||||
|
public long SpeedLimit { get; set; } = long.MaxValue; // 限速设置
|
||||||
|
public long? ResponseLength { get; set; }
|
||||||
|
public long RDownloaded => _Rdownloaded;
|
||||||
|
private int _zeroSpeedCount = 0;
|
||||||
|
public int LowSpeedCount => _zeroSpeedCount;
|
||||||
|
public bool ShouldStop => LowSpeedCount >= 20;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private long _downloaded = 0;
|
||||||
|
private long _Rdownloaded = 0;
|
||||||
|
public long Downloaded => _downloaded;
|
||||||
|
|
||||||
|
public int AddLowSpeedCount()
|
||||||
{
|
{
|
||||||
public bool SingleSegment { get; set; } = false;
|
return Interlocked.Add(ref _zeroSpeedCount, 1);
|
||||||
public long NowSpeed { get; set; } = 0L; //当前每秒速度
|
|
||||||
public long SpeedLimit { get; set; } = long.MaxValue; //限速设置
|
|
||||||
public long? ResponseLength { get; set; }
|
|
||||||
public long RDownloaded { get => _Rdownloaded; }
|
|
||||||
private int _zeroSpeedCount = 0;
|
|
||||||
public int LowSpeedCount { get => _zeroSpeedCount; }
|
|
||||||
public bool ShouldStop { get => LowSpeedCount >= 20; }
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////
|
|
||||||
|
|
||||||
private long _downloaded = 0;
|
|
||||||
private long _Rdownloaded = 0;
|
|
||||||
public long Downloaded { get => _downloaded; }
|
|
||||||
|
|
||||||
public int AddLowSpeedCount()
|
|
||||||
{
|
|
||||||
return Interlocked.Add(ref _zeroSpeedCount, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int ResetLowSpeedCount()
|
|
||||||
{
|
|
||||||
return Interlocked.Exchange(ref _zeroSpeedCount, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public long Add(long size)
|
|
||||||
{
|
|
||||||
Interlocked.Add(ref _Rdownloaded, size);
|
|
||||||
return Interlocked.Add(ref _downloaded, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref _downloaded, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetVars()
|
|
||||||
{
|
|
||||||
Reset();
|
|
||||||
ResetLowSpeedCount();
|
|
||||||
SingleSegment = false;
|
|
||||||
ResponseLength = null;
|
|
||||||
_Rdownloaded = 0L;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public int ResetLowSpeedCount()
|
||||||
|
{
|
||||||
|
return Interlocked.Exchange(ref _zeroSpeedCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Add(long size)
|
||||||
|
{
|
||||||
|
Interlocked.Add(ref _Rdownloaded, size);
|
||||||
|
return Interlocked.Add(ref _downloaded, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _downloaded, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetVars()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
ResetLowSpeedCount();
|
||||||
|
SingleSegment = false;
|
||||||
|
ResponseLength = null;
|
||||||
|
_Rdownloaded = 0L;
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +1,51 @@
|
|||||||
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? LanguageReg { get; set; }
|
||||||
|
public Regex? NameReg { get; set; }
|
||||||
|
public Regex? CodecsReg { get; set; }
|
||||||
|
public Regex? ResolutionReg { get; set; }
|
||||||
|
public Regex? FrameRateReg { get; set; }
|
||||||
|
public Regex? ChannelsReg { get; set; }
|
||||||
|
public Regex? VideoRangeReg { get; set; }
|
||||||
|
public Regex? UrlReg { get; set; }
|
||||||
|
public long? SegmentsMinCount { get; set; }
|
||||||
|
public long? SegmentsMaxCount { get; set; }
|
||||||
|
public double? PlaylistMinDur { get; set; }
|
||||||
|
public double? PlaylistMaxDur { get; set; }
|
||||||
|
public int? BandwidthMin { get; set; }
|
||||||
|
public int? BandwidthMax { get; set; }
|
||||||
|
public RoleType? Role { get; set; }
|
||||||
|
|
||||||
|
public string For { get; set; } = "best";
|
||||||
|
|
||||||
|
public override string? ToString()
|
||||||
{
|
{
|
||||||
public Regex? GroupIdReg { get; set; }
|
var sb = new StringBuilder();
|
||||||
public Regex? LanguageReg { get; set; }
|
|
||||||
public Regex? NameReg { get; set; }
|
|
||||||
public Regex? CodecsReg { get; set; }
|
|
||||||
public Regex? ResolutionReg { get; set; }
|
|
||||||
public Regex? FrameRateReg { get; set; }
|
|
||||||
public Regex? ChannelsReg { get; set; }
|
|
||||||
public Regex? VideoRangeReg { get; set; }
|
|
||||||
public Regex? UrlReg { get; set; }
|
|
||||||
public long? SegmentsMinCount { get; set; }
|
|
||||||
public long? SegmentsMaxCount { get; set; }
|
|
||||||
public double? PlaylistMinDur { get; set; }
|
|
||||||
public double? PlaylistMaxDur { get; set; }
|
|
||||||
public int? BandwidthMin { get; set; }
|
|
||||||
public int? BandwidthMax { get; set; }
|
|
||||||
public RoleType? Role { get; set; }
|
|
||||||
|
|
||||||
public string For { get; set; } = "best";
|
if (GroupIdReg != null) sb.Append($"GroupIdReg: {GroupIdReg} ");
|
||||||
|
if (LanguageReg != null) sb.Append($"LanguageReg: {LanguageReg} ");
|
||||||
|
if (NameReg != null) sb.Append($"NameReg: {NameReg} ");
|
||||||
|
if (CodecsReg != null) sb.Append($"CodecsReg: {CodecsReg} ");
|
||||||
|
if (ResolutionReg != null) sb.Append($"ResolutionReg: {ResolutionReg} ");
|
||||||
|
if (FrameRateReg != null) sb.Append($"FrameRateReg: {FrameRateReg} ");
|
||||||
|
if (ChannelsReg != null) sb.Append($"ChannelsReg: {ChannelsReg} ");
|
||||||
|
if (VideoRangeReg != null) sb.Append($"VideoRangeReg: {VideoRangeReg} ");
|
||||||
|
if (UrlReg != null) sb.Append($"UrlReg: {UrlReg} ");
|
||||||
|
if (SegmentsMinCount != null) sb.Append($"SegmentsMinCount: {SegmentsMinCount} ");
|
||||||
|
if (SegmentsMaxCount != null) sb.Append($"SegmentsMaxCount: {SegmentsMaxCount} ");
|
||||||
|
if (PlaylistMinDur != null) sb.Append($"PlaylistMinDur: {PlaylistMinDur} ");
|
||||||
|
if (PlaylistMaxDur != null) sb.Append($"PlaylistMaxDur: {PlaylistMaxDur} ");
|
||||||
|
if (BandwidthMin != null) sb.Append($"{nameof(BandwidthMin)}: {BandwidthMin} ");
|
||||||
|
if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} ");
|
||||||
|
if (Role.HasValue) sb.Append($"Role: {Role} ");
|
||||||
|
|
||||||
public override string? ToString()
|
return sb + $"For: {For}";
|
||||||
{
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
|
|
||||||
if (GroupIdReg != null) sb.Append($"GroupIdReg: {GroupIdReg} ");
|
|
||||||
if (LanguageReg != null) sb.Append($"LanguageReg: {LanguageReg} ");
|
|
||||||
if (NameReg != null) sb.Append($"NameReg: {NameReg} ");
|
|
||||||
if (CodecsReg != null) sb.Append($"CodecsReg: {CodecsReg} ");
|
|
||||||
if (ResolutionReg != null) sb.Append($"ResolutionReg: {ResolutionReg} ");
|
|
||||||
if (FrameRateReg != null) sb.Append($"FrameRateReg: {FrameRateReg} ");
|
|
||||||
if (ChannelsReg != null) sb.Append($"ChannelsReg: {ChannelsReg} ");
|
|
||||||
if (VideoRangeReg != null) sb.Append($"VideoRangeReg: {VideoRangeReg} ");
|
|
||||||
if (UrlReg != null) sb.Append($"UrlReg: {UrlReg} ");
|
|
||||||
if (SegmentsMinCount != null) sb.Append($"SegmentsMinCount: {SegmentsMinCount} ");
|
|
||||||
if (SegmentsMaxCount != null) sb.Append($"SegmentsMaxCount: {SegmentsMaxCount} ");
|
|
||||||
if (PlaylistMinDur != null) sb.Append($"PlaylistMinDur: {PlaylistMinDur} ");
|
|
||||||
if (PlaylistMaxDur != null) sb.Append($"PlaylistMaxDur: {PlaylistMaxDur} ");
|
|
||||||
if (BandwidthMin != null) sb.Append($"{nameof(BandwidthMin)}: {BandwidthMin} ");
|
|
||||||
if (BandwidthMax != null) sb.Append($"{nameof(BandwidthMax)}: {BandwidthMax} ");
|
|
||||||
if (Role.HasValue) sb.Append($"Role: {Role} ");
|
|
||||||
|
|
||||||
return sb.ToString() + $"For: {For}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
8
src/N_m3u8DL-RE/Enum/DecryptEngine.cs
Normal file
8
src/N_m3u8DL-RE/Enum/DecryptEngine.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace N_m3u8DL_RE.Enum;
|
||||||
|
|
||||||
|
internal enum DecryptEngine
|
||||||
|
{
|
||||||
|
MP4DECRYPT,
|
||||||
|
SHAKA_PACKAGER,
|
||||||
|
FFMPEG,
|
||||||
|
}
|
8
src/N_m3u8DL-RE/Enum/MuxFormat.cs
Normal file
8
src/N_m3u8DL-RE/Enum/MuxFormat.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace N_m3u8DL_RE.Enum;
|
||||||
|
|
||||||
|
internal enum MuxFormat
|
||||||
|
{
|
||||||
|
MP4,
|
||||||
|
MKV,
|
||||||
|
TS,
|
||||||
|
}
|
@ -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,
|
||||||
{
|
SRT
|
||||||
VTT,
|
}
|
||||||
SRT
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -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>
|
||||||
|
|
||||||
|
@ -2,26 +2,20 @@
|
|||||||
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)
|
||||||
{
|
{
|
||||||
|
return extractorType == ExtractorType.MPEG_DASH && parserConfig.Url.Contains("bitmovin");
|
||||||
public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig)
|
|
||||||
{
|
|
||||||
return extractorType == ExtractorType.MPEG_DASH && parserConfig.Url.Contains("bitmovin");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string Process(string rawText, ParserConfig parserConfig)
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp("[red]Match bitmovin![/]");
|
|
||||||
return rawText;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override string Process(string rawText, ParserConfig parserConfig)
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp("[red]Match bitmovin![/]");
|
||||||
|
return rawText;
|
||||||
|
}
|
||||||
|
}
|
@ -5,27 +5,21 @@ 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp($"[white on green]My Key Processor => {keyLine}[/]");
|
|
||||||
var info = new DefaultHLSKeyProcessor().Process(keyLine, m3u8Url, m3u8Content, parserConfig);
|
|
||||||
Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]");
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override EncryptInfo Process(string keyLine, string m3u8Url, string m3u8Content, ParserConfig parserConfig)
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp($"[white on green]My Key Processor => {keyLine}[/]");
|
||||||
|
var info = new DefaultHLSKeyProcessor().Process(keyLine, m3u8Url, m3u8Content, parserConfig);
|
||||||
|
Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(info.Key!, " ") + "[/]");
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
@ -6,219 +6,211 @@ 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"
|
private static string START = "nowehoryzonty:";
|
||||||
internal class NowehoryzontyUrlProcessor : UrlProcessor
|
private static string? TimeDifferenceStr = null;
|
||||||
|
private static int? TimeDifference = null;
|
||||||
|
private static string? SecureToken = null;
|
||||||
|
private static bool LOG = false;
|
||||||
|
private static Function? Function = null;
|
||||||
|
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig)
|
||||||
{
|
{
|
||||||
private static string START = "nowehoryzonty:";
|
if (extractorType == ExtractorType.MPEG_DASH && parserConfig.UrlProcessorArgs != null && parserConfig.UrlProcessorArgs.StartsWith(START))
|
||||||
private static string? TimeDifferenceStr = null;
|
|
||||||
private static int? TimeDifference = null;
|
|
||||||
private static string? SecureToken = null;
|
|
||||||
private static bool LOG = false;
|
|
||||||
private static Function? Function = null;
|
|
||||||
public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig)
|
|
||||||
{
|
{
|
||||||
if (extractorType == ExtractorType.MPEG_DASH && parserConfig.UrlProcessorArgs != null && parserConfig.UrlProcessorArgs.StartsWith(START))
|
if (!LOG)
|
||||||
{
|
{
|
||||||
if (!LOG)
|
Logger.WarnMarkUp($"[white on green]www.nowehoryzonty.pl[/] matched! waiting for calc...");
|
||||||
{
|
LOG = true;
|
||||||
Logger.WarnMarkUp($"[white on green]www.nowehoryzonty.pl[/] matched! waiting for calc...");
|
|
||||||
LOG = true;
|
|
||||||
}
|
|
||||||
var context = new Context();
|
|
||||||
context.Eval(JS);
|
|
||||||
Function = context.GetVariable("md5").As<Function>();
|
|
||||||
var argLine = parserConfig.UrlProcessorArgs![START.Length..];
|
|
||||||
TimeDifferenceStr = ParserUtil.GetAttribute(argLine, "timeDifference");
|
|
||||||
SecureToken = ParserUtil.GetAttribute(argLine, "filminfo.secureToken");
|
|
||||||
if (TimeDifferenceStr != null && SecureToken != null)
|
|
||||||
{
|
|
||||||
TimeDifference = Convert.ToInt32(TimeDifferenceStr);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
var context = new Context();
|
||||||
|
context.Eval(JS);
|
||||||
|
Function = context.GetVariable("md5").As<Function>();
|
||||||
|
var argLine = parserConfig.UrlProcessorArgs![START.Length..];
|
||||||
|
TimeDifferenceStr = ParserUtil.GetAttribute(argLine, "timeDifference");
|
||||||
|
SecureToken = ParserUtil.GetAttribute(argLine, "filminfo.secureToken");
|
||||||
|
if (TimeDifferenceStr != null && SecureToken != null)
|
||||||
|
{
|
||||||
|
TimeDifference = Convert.ToInt32(TimeDifferenceStr);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
public override string Process(string oriUrl, ParserConfig parserConfig)
|
|
||||||
{
|
|
||||||
var a = new Uri(oriUrl).AbsolutePath;
|
|
||||||
var n = oriUrl + "?secure=" + Calc(a);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Calc(string a)
|
|
||||||
{
|
|
||||||
string returnStr = Function!.Call(new Arguments { a, SecureToken, TimeDifference }).ToString();
|
|
||||||
return returnStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
////https://www.nowehoryzonty.pl/packed/videonho.js?v=1114377281:formatted
|
|
||||||
private static readonly string JS = """
|
|
||||||
var p = function(f, e) {
|
|
||||||
var d = f[0]
|
|
||||||
, a = f[1]
|
|
||||||
, b = f[2]
|
|
||||||
, c = f[3];
|
|
||||||
d = h(d, a, b, c, e[0], 7, -680876936);
|
|
||||||
c = h(c, d, a, b, e[1], 12, -389564586);
|
|
||||||
b = h(b, c, d, a, e[2], 17, 606105819);
|
|
||||||
a = h(a, b, c, d, e[3], 22, -1044525330);
|
|
||||||
d = h(d, a, b, c, e[4], 7, -176418897);
|
|
||||||
c = h(c, d, a, b, e[5], 12, 1200080426);
|
|
||||||
b = h(b, c, d, a, e[6], 17, -1473231341);
|
|
||||||
a = h(a, b, c, d, e[7], 22, -45705983);
|
|
||||||
d = h(d, a, b, c, e[8], 7, 1770035416);
|
|
||||||
c = h(c, d, a, b, e[9], 12, -1958414417);
|
|
||||||
b = h(b, c, d, a, e[10], 17, -42063);
|
|
||||||
a = h(a, b, c, d, e[11], 22, -1990404162);
|
|
||||||
d = h(d, a, b, c, e[12], 7, 1804603682);
|
|
||||||
c = h(c, d, a, b, e[13], 12, -40341101);
|
|
||||||
b = h(b, c, d, a, e[14], 17, -1502002290);
|
|
||||||
a = h(a, b, c, d, e[15], 22, 1236535329);
|
|
||||||
d = k(d, a, b, c, e[1], 5, -165796510);
|
|
||||||
c = k(c, d, a, b, e[6], 9, -1069501632);
|
|
||||||
b = k(b, c, d, a, e[11], 14, 643717713);
|
|
||||||
a = k(a, b, c, d, e[0], 20, -373897302);
|
|
||||||
d = k(d, a, b, c, e[5], 5, -701558691);
|
|
||||||
c = k(c, d, a, b, e[10], 9, 38016083);
|
|
||||||
b = k(b, c, d, a, e[15], 14, -660478335);
|
|
||||||
a = k(a, b, c, d, e[4], 20, -405537848);
|
|
||||||
d = k(d, a, b, c, e[9], 5, 568446438);
|
|
||||||
c = k(c, d, a, b, e[14], 9, -1019803690);
|
|
||||||
b = k(b, c, d, a, e[3], 14, -187363961);
|
|
||||||
a = k(a, b, c, d, e[8], 20, 1163531501);
|
|
||||||
d = k(d, a, b, c, e[13], 5, -1444681467);
|
|
||||||
c = k(c, d, a, b, e[2], 9, -51403784);
|
|
||||||
b = k(b, c, d, a, e[7], 14, 1735328473);
|
|
||||||
a = k(a, b, c, d, e[12], 20, -1926607734);
|
|
||||||
d = g(a ^ b ^ c, d, a, e[5], 4, -378558);
|
|
||||||
c = g(d ^ a ^ b, c, d, e[8], 11, -2022574463);
|
|
||||||
b = g(c ^ d ^ a, b, c, e[11], 16, 1839030562);
|
|
||||||
a = g(b ^ c ^ d, a, b, e[14], 23, -35309556);
|
|
||||||
d = g(a ^ b ^ c, d, a, e[1], 4, -1530992060);
|
|
||||||
c = g(d ^ a ^ b, c, d, e[4], 11, 1272893353);
|
|
||||||
b = g(c ^ d ^ a, b, c, e[7], 16, -155497632);
|
|
||||||
a = g(b ^ c ^ d, a, b, e[10], 23, -1094730640);
|
|
||||||
d = g(a ^ b ^ c, d, a, e[13], 4, 681279174);
|
|
||||||
c = g(d ^ a ^ b, c, d, e[0], 11, -358537222);
|
|
||||||
b = g(c ^ d ^ a, b, c, e[3], 16, -722521979);
|
|
||||||
a = g(b ^ c ^ d, a, b, e[6], 23, 76029189);
|
|
||||||
d = g(a ^ b ^ c, d, a, e[9], 4, -640364487);
|
|
||||||
c = g(d ^ a ^ b, c, d, e[12], 11, -421815835);
|
|
||||||
b = g(c ^ d ^ a, b, c, e[15], 16, 530742520);
|
|
||||||
a = g(b ^ c ^ d, a, b, e[2], 23, -995338651);
|
|
||||||
d = l(d, a, b, c, e[0], 6, -198630844);
|
|
||||||
c = l(c, d, a, b, e[7], 10, 1126891415);
|
|
||||||
b = l(b, c, d, a, e[14], 15, -1416354905);
|
|
||||||
a = l(a, b, c, d, e[5], 21, -57434055);
|
|
||||||
d = l(d, a, b, c, e[12], 6, 1700485571);
|
|
||||||
c = l(c, d, a, b, e[3], 10, -1894986606);
|
|
||||||
b = l(b, c, d, a, e[10], 15, -1051523);
|
|
||||||
a = l(a, b, c, d, e[1], 21, -2054922799);
|
|
||||||
d = l(d, a, b, c, e[8], 6, 1873313359);
|
|
||||||
c = l(c, d, a, b, e[15], 10, -30611744);
|
|
||||||
b = l(b, c, d, a, e[6], 15, -1560198380);
|
|
||||||
a = l(a, b, c, d, e[13], 21, 1309151649);
|
|
||||||
d = l(d, a, b, c, e[4], 6, -145523070);
|
|
||||||
c = l(c, d, a, b, e[11], 10, -1120210379);
|
|
||||||
b = l(b, c, d, a, e[2], 15, 718787259);
|
|
||||||
a = l(a, b, c, d, e[9], 21, -343485551);
|
|
||||||
f[0] = m(d, f[0]);
|
|
||||||
f[1] = m(a, f[1]);
|
|
||||||
f[2] = m(b, f[2]);
|
|
||||||
f[3] = m(c, f[3])
|
|
||||||
}, g = function(f, e, d, a, b, c) {
|
|
||||||
e = m(m(e, f), m(a, c));
|
|
||||||
return m(e << b | e >>> 32 - b, d)
|
|
||||||
}
|
|
||||||
, h = function(f, e, d, a, b, c, n) {
|
|
||||||
return g(e & d | ~e & a, f, e, b, c, n)
|
|
||||||
}
|
|
||||||
, k = function(f, e, d, a, b, c, n) {
|
|
||||||
return g(e & a | d & ~a, f, e, b, c, n)
|
|
||||||
}
|
|
||||||
, l = function(f, e, d, a, b, c, n) {
|
|
||||||
return g(d ^ (e | ~a), f, e, b, c, n)
|
|
||||||
}, r = "0123456789abcdef".split("");
|
|
||||||
|
|
||||||
var m = function(f, e) {
|
|
||||||
return f + e & 4294967295
|
|
||||||
};
|
|
||||||
|
|
||||||
var q = function(f) {
|
|
||||||
var e = f.length, d = [1732584193, -271733879, -1732584194, 271733878], a;
|
|
||||||
for (a = 64; a <= f.length; a += 64) {
|
|
||||||
var b, c = f.substring(a - 64, a), g = [];
|
|
||||||
for (b = 0; 64 > b; b += 4)
|
|
||||||
g[b >> 2] = c.charCodeAt(b) + (c.charCodeAt(b + 1) << 8) + (c.charCodeAt(b + 2) << 16) + (c.charCodeAt(b + 3) << 24);
|
|
||||||
p(d, g)
|
|
||||||
}
|
|
||||||
f = f.substring(a - 64);
|
|
||||||
b = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
||||||
for (a = 0; a < f.length; a++)
|
|
||||||
b[a >> 2] |= f.charCodeAt(a) << (a % 4 << 3);
|
|
||||||
b[a >> 2] |= 128 << (a % 4 << 3);
|
|
||||||
if (55 < a)
|
|
||||||
for (p(d, b),
|
|
||||||
a = 0; 16 > a; a++)
|
|
||||||
b[a] = 0;
|
|
||||||
b[14] = 8 * e;
|
|
||||||
p(d, b);
|
|
||||||
return d
|
|
||||||
};
|
|
||||||
|
|
||||||
var md5 = function(f, e, timeDifference) {
|
|
||||||
var d = Date.now() + 6E4 + timeDifference;
|
|
||||||
e = q(d + f + e);
|
|
||||||
f = [];
|
|
||||||
for (var a = 0; a < e.length; a++) {
|
|
||||||
var b = e[a];
|
|
||||||
var c = []
|
|
||||||
, g = 4;
|
|
||||||
do
|
|
||||||
c[--g] = b & 255,
|
|
||||||
b >>= 8;
|
|
||||||
while (g);
|
|
||||||
b = c;
|
|
||||||
for (c = b.length - 1; 0 <= c; c--)
|
|
||||||
f.push(b[c])
|
|
||||||
}
|
|
||||||
g = void 0;
|
|
||||||
c = "";
|
|
||||||
for (e = a = b = 0; e < 4 * f.length / 3; g = b >> 2 * (++e & 3) & 63,
|
|
||||||
c += String.fromCharCode(g + 71 - (26 > g ? 6 : 52 > g ? 0 : 62 > g ? 75 : g ^ 63 ? 90 : 87)) + (75 == (e - 1) % 76 ? "\r\n" : ""))
|
|
||||||
e & 3 ^ 3 && (b = b << 8 ^ f[a++]);
|
|
||||||
for (; e++ & 3; )
|
|
||||||
c += "\x3d";
|
|
||||||
return c.replace(/\+/g, "-").replace(/\//g, "_") + "," + d
|
|
||||||
};
|
|
||||||
|
|
||||||
"5d41402abc4b2a76b9719d911017c592" != function(f) {
|
|
||||||
for (var e = 0; e < f.length; e++) {
|
|
||||||
for (var d = e, a = f[e], b = "", c = 0; 4 > c; c++)
|
|
||||||
b += r[a >> 8 * c + 4 & 15] + r[a >> 8 * c & 15];
|
|
||||||
f[d] = b
|
|
||||||
}
|
|
||||||
return f.join("")
|
|
||||||
}(q("hello")) && (m = function(f, e) {
|
|
||||||
var d = (f & 65535) + (e & 65535);
|
|
||||||
return (f >> 16) + (e >> 16) + (d >> 16) << 16 | d & 65535
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/h264.mpd','vx54axqjal4f0yy2',-2274));
|
|
||||||
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/subtitle_pl/34.m4s','vx54axqjal4f0yy2',-2274));
|
|
||||||
|
|
||||||
""";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public override string Process(string oriUrl, ParserConfig parserConfig)
|
||||||
|
{
|
||||||
|
var a = new Uri(oriUrl).AbsolutePath;
|
||||||
|
var n = oriUrl + "?secure=" + Calc(a);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Calc(string a)
|
||||||
|
{
|
||||||
|
string returnStr = Function!.Call(new Arguments { a, SecureToken, TimeDifference }).ToString();
|
||||||
|
return returnStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
////https://www.nowehoryzonty.pl/packed/videonho.js?v=1114377281:formatted
|
||||||
|
private static readonly string JS = """
|
||||||
|
var p = function(f, e) {
|
||||||
|
var d = f[0]
|
||||||
|
, a = f[1]
|
||||||
|
, b = f[2]
|
||||||
|
, c = f[3];
|
||||||
|
d = h(d, a, b, c, e[0], 7, -680876936);
|
||||||
|
c = h(c, d, a, b, e[1], 12, -389564586);
|
||||||
|
b = h(b, c, d, a, e[2], 17, 606105819);
|
||||||
|
a = h(a, b, c, d, e[3], 22, -1044525330);
|
||||||
|
d = h(d, a, b, c, e[4], 7, -176418897);
|
||||||
|
c = h(c, d, a, b, e[5], 12, 1200080426);
|
||||||
|
b = h(b, c, d, a, e[6], 17, -1473231341);
|
||||||
|
a = h(a, b, c, d, e[7], 22, -45705983);
|
||||||
|
d = h(d, a, b, c, e[8], 7, 1770035416);
|
||||||
|
c = h(c, d, a, b, e[9], 12, -1958414417);
|
||||||
|
b = h(b, c, d, a, e[10], 17, -42063);
|
||||||
|
a = h(a, b, c, d, e[11], 22, -1990404162);
|
||||||
|
d = h(d, a, b, c, e[12], 7, 1804603682);
|
||||||
|
c = h(c, d, a, b, e[13], 12, -40341101);
|
||||||
|
b = h(b, c, d, a, e[14], 17, -1502002290);
|
||||||
|
a = h(a, b, c, d, e[15], 22, 1236535329);
|
||||||
|
d = k(d, a, b, c, e[1], 5, -165796510);
|
||||||
|
c = k(c, d, a, b, e[6], 9, -1069501632);
|
||||||
|
b = k(b, c, d, a, e[11], 14, 643717713);
|
||||||
|
a = k(a, b, c, d, e[0], 20, -373897302);
|
||||||
|
d = k(d, a, b, c, e[5], 5, -701558691);
|
||||||
|
c = k(c, d, a, b, e[10], 9, 38016083);
|
||||||
|
b = k(b, c, d, a, e[15], 14, -660478335);
|
||||||
|
a = k(a, b, c, d, e[4], 20, -405537848);
|
||||||
|
d = k(d, a, b, c, e[9], 5, 568446438);
|
||||||
|
c = k(c, d, a, b, e[14], 9, -1019803690);
|
||||||
|
b = k(b, c, d, a, e[3], 14, -187363961);
|
||||||
|
a = k(a, b, c, d, e[8], 20, 1163531501);
|
||||||
|
d = k(d, a, b, c, e[13], 5, -1444681467);
|
||||||
|
c = k(c, d, a, b, e[2], 9, -51403784);
|
||||||
|
b = k(b, c, d, a, e[7], 14, 1735328473);
|
||||||
|
a = k(a, b, c, d, e[12], 20, -1926607734);
|
||||||
|
d = g(a ^ b ^ c, d, a, e[5], 4, -378558);
|
||||||
|
c = g(d ^ a ^ b, c, d, e[8], 11, -2022574463);
|
||||||
|
b = g(c ^ d ^ a, b, c, e[11], 16, 1839030562);
|
||||||
|
a = g(b ^ c ^ d, a, b, e[14], 23, -35309556);
|
||||||
|
d = g(a ^ b ^ c, d, a, e[1], 4, -1530992060);
|
||||||
|
c = g(d ^ a ^ b, c, d, e[4], 11, 1272893353);
|
||||||
|
b = g(c ^ d ^ a, b, c, e[7], 16, -155497632);
|
||||||
|
a = g(b ^ c ^ d, a, b, e[10], 23, -1094730640);
|
||||||
|
d = g(a ^ b ^ c, d, a, e[13], 4, 681279174);
|
||||||
|
c = g(d ^ a ^ b, c, d, e[0], 11, -358537222);
|
||||||
|
b = g(c ^ d ^ a, b, c, e[3], 16, -722521979);
|
||||||
|
a = g(b ^ c ^ d, a, b, e[6], 23, 76029189);
|
||||||
|
d = g(a ^ b ^ c, d, a, e[9], 4, -640364487);
|
||||||
|
c = g(d ^ a ^ b, c, d, e[12], 11, -421815835);
|
||||||
|
b = g(c ^ d ^ a, b, c, e[15], 16, 530742520);
|
||||||
|
a = g(b ^ c ^ d, a, b, e[2], 23, -995338651);
|
||||||
|
d = l(d, a, b, c, e[0], 6, -198630844);
|
||||||
|
c = l(c, d, a, b, e[7], 10, 1126891415);
|
||||||
|
b = l(b, c, d, a, e[14], 15, -1416354905);
|
||||||
|
a = l(a, b, c, d, e[5], 21, -57434055);
|
||||||
|
d = l(d, a, b, c, e[12], 6, 1700485571);
|
||||||
|
c = l(c, d, a, b, e[3], 10, -1894986606);
|
||||||
|
b = l(b, c, d, a, e[10], 15, -1051523);
|
||||||
|
a = l(a, b, c, d, e[1], 21, -2054922799);
|
||||||
|
d = l(d, a, b, c, e[8], 6, 1873313359);
|
||||||
|
c = l(c, d, a, b, e[15], 10, -30611744);
|
||||||
|
b = l(b, c, d, a, e[6], 15, -1560198380);
|
||||||
|
a = l(a, b, c, d, e[13], 21, 1309151649);
|
||||||
|
d = l(d, a, b, c, e[4], 6, -145523070);
|
||||||
|
c = l(c, d, a, b, e[11], 10, -1120210379);
|
||||||
|
b = l(b, c, d, a, e[2], 15, 718787259);
|
||||||
|
a = l(a, b, c, d, e[9], 21, -343485551);
|
||||||
|
f[0] = m(d, f[0]);
|
||||||
|
f[1] = m(a, f[1]);
|
||||||
|
f[2] = m(b, f[2]);
|
||||||
|
f[3] = m(c, f[3])
|
||||||
|
}, g = function(f, e, d, a, b, c) {
|
||||||
|
e = m(m(e, f), m(a, c));
|
||||||
|
return m(e << b | e >>> 32 - b, d)
|
||||||
|
}
|
||||||
|
, h = function(f, e, d, a, b, c, n) {
|
||||||
|
return g(e & d | ~e & a, f, e, b, c, n)
|
||||||
|
}
|
||||||
|
, k = function(f, e, d, a, b, c, n) {
|
||||||
|
return g(e & a | d & ~a, f, e, b, c, n)
|
||||||
|
}
|
||||||
|
, l = function(f, e, d, a, b, c, n) {
|
||||||
|
return g(d ^ (e | ~a), f, e, b, c, n)
|
||||||
|
}, r = "0123456789abcdef".split("");
|
||||||
|
|
||||||
|
var m = function(f, e) {
|
||||||
|
return f + e & 4294967295
|
||||||
|
};
|
||||||
|
|
||||||
|
var q = function(f) {
|
||||||
|
var e = f.length, d = [1732584193, -271733879, -1732584194, 271733878], a;
|
||||||
|
for (a = 64; a <= f.length; a += 64) {
|
||||||
|
var b, c = f.substring(a - 64, a), g = [];
|
||||||
|
for (b = 0; 64 > b; b += 4)
|
||||||
|
g[b >> 2] = c.charCodeAt(b) + (c.charCodeAt(b + 1) << 8) + (c.charCodeAt(b + 2) << 16) + (c.charCodeAt(b + 3) << 24);
|
||||||
|
p(d, g)
|
||||||
|
}
|
||||||
|
f = f.substring(a - 64);
|
||||||
|
b = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
for (a = 0; a < f.length; a++)
|
||||||
|
b[a >> 2] |= f.charCodeAt(a) << (a % 4 << 3);
|
||||||
|
b[a >> 2] |= 128 << (a % 4 << 3);
|
||||||
|
if (55 < a)
|
||||||
|
for (p(d, b),
|
||||||
|
a = 0; 16 > a; a++)
|
||||||
|
b[a] = 0;
|
||||||
|
b[14] = 8 * e;
|
||||||
|
p(d, b);
|
||||||
|
return d
|
||||||
|
};
|
||||||
|
|
||||||
|
var md5 = function(f, e, timeDifference) {
|
||||||
|
var d = Date.now() + 6E4 + timeDifference;
|
||||||
|
e = q(d + f + e);
|
||||||
|
f = [];
|
||||||
|
for (var a = 0; a < e.length; a++) {
|
||||||
|
var b = e[a];
|
||||||
|
var c = []
|
||||||
|
, g = 4;
|
||||||
|
do
|
||||||
|
c[--g] = b & 255,
|
||||||
|
b >>= 8;
|
||||||
|
while (g);
|
||||||
|
b = c;
|
||||||
|
for (c = b.length - 1; 0 <= c; c--)
|
||||||
|
f.push(b[c])
|
||||||
|
}
|
||||||
|
g = void 0;
|
||||||
|
c = "";
|
||||||
|
for (e = a = b = 0; e < 4 * f.length / 3; g = b >> 2 * (++e & 3) & 63,
|
||||||
|
c += String.fromCharCode(g + 71 - (26 > g ? 6 : 52 > g ? 0 : 62 > g ? 75 : g ^ 63 ? 90 : 87)) + (75 == (e - 1) % 76 ? "\r\n" : ""))
|
||||||
|
e & 3 ^ 3 && (b = b << 8 ^ f[a++]);
|
||||||
|
for (; e++ & 3; )
|
||||||
|
c += "\x3d";
|
||||||
|
return c.replace(/\+/g, "-").replace(/\//g, "_") + "," + d
|
||||||
|
};
|
||||||
|
|
||||||
|
"5d41402abc4b2a76b9719d911017c592" != function(f) {
|
||||||
|
for (var e = 0; e < f.length; e++) {
|
||||||
|
for (var d = e, a = f[e], b = "", c = 0; 4 > c; c++)
|
||||||
|
b += r[a >> 8 * c + 4 & 15] + r[a >> 8 * c & 15];
|
||||||
|
f[d] = b
|
||||||
|
}
|
||||||
|
return f.join("")
|
||||||
|
}(q("hello")) && (m = function(f, e) {
|
||||||
|
var d = (f & 65535) + (e & 65535);
|
||||||
|
return (f >> 16) + (e >> 16) + (d >> 16) << 16 | d & 65535
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/h264.mpd','vx54axqjal4f0yy2',-2274));
|
||||||
|
//console.log(md5('/r/nh22/2022/VNUS_DE_NYKE/19_07_22_2302_skt/subtitle_pl/34.m4s','vx54axqjal4f0yy2',-2274));
|
||||||
|
|
||||||
|
""";
|
||||||
|
}
|
@ -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,439 +14,469 @@ 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())
|
||||||
{
|
{
|
||||||
Console.CancelKeyPress += Console_CancelKeyPress;
|
var osVersion = Environment.OSVersion.Version;
|
||||||
ServicePointManager.DefaultConnectionLimit = 1024;
|
if (osVersion.Major < 6 || osVersion is { Major: 6, Minor: 0 })
|
||||||
try { Console.CursorVisible = true; } catch { }
|
|
||||||
|
|
||||||
string loc = "en-US";
|
|
||||||
string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
|
|
||||||
if (currLoc == "zh-CN" || currLoc == "zh-SG") loc = "zh-CN";
|
|
||||||
else if (currLoc.StartsWith("zh-")) loc = "zh-TW";
|
|
||||||
|
|
||||||
//处理用户-h等请求
|
|
||||||
var index = -1;
|
|
||||||
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]))
|
|
||||||
{
|
{
|
||||||
loc = list[index + 1];
|
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.CancelKeyPress += Console_CancelKeyPress;
|
||||||
|
ServicePointManager.DefaultConnectionLimit = 1024;
|
||||||
|
try { Console.CursorVisible = true; } catch { }
|
||||||
|
|
||||||
|
string loc = ResString.CurrentLoc;
|
||||||
|
string currLoc = Thread.CurrentThread.CurrentUICulture.Name;
|
||||||
|
if (currLoc is "zh-CN" or "zh-SG") loc = "zh-CN";
|
||||||
|
else if (currLoc.StartsWith("zh-")) loc = "zh-TW";
|
||||||
|
|
||||||
|
// 处理用户-h等请求
|
||||||
|
var index = -1;
|
||||||
|
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]))
|
||||||
|
{
|
||||||
|
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
|
||||||
await CommandInvoker.InvokeArgs(args, DoWorkAsync);
|
{
|
||||||
|
// Culture not work on NT6.0, so catch the exception
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
|
await CommandInvoker.InvokeArgs(args, DoWorkAsync);
|
||||||
{
|
}
|
||||||
Logger.WarnMarkUp("Force Exit...");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.CursorVisible = true;
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
System.Diagnostics.Process.Start("stty", "echo");
|
|
||||||
} catch { }
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int GetOrder(StreamSpec streamSpec)
|
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
|
||||||
{
|
{
|
||||||
if (streamSpec.Channels == null) return 0;
|
Logger.WarnMarkUp("Force Exit...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Console.CursorVisible = true;
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
System.Diagnostics.Process.Start("tput", "cnorm");
|
||||||
|
} catch { }
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int GetOrder(StreamSpec streamSpec)
|
||||||
|
{
|
||||||
|
if (streamSpec.Channels == null) return 0;
|
||||||
|
|
||||||
var str = streamSpec.Channels.Split('/')[0];
|
var str = streamSpec.Channels.Split('/')[0];
|
||||||
return int.TryParse(str, out var order) ? order : 0;
|
return int.TryParse(str, out var order) ? order : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task DoWorkAsync(MyOption option)
|
||||||
|
{
|
||||||
|
HTTPUtil.AppHttpClient.Timeout = TimeSpan.FromSeconds(option.HttpRequestTimeout);
|
||||||
|
if (Console.IsOutputRedirected || Console.IsErrorRedirected)
|
||||||
|
{
|
||||||
|
option.ForceAnsiConsole = true;
|
||||||
|
option.NoAnsiColor = true;
|
||||||
|
Logger.Info(ResString.consoleRedirected);
|
||||||
|
}
|
||||||
|
CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor);
|
||||||
|
|
||||||
|
// 检测更新
|
||||||
|
if (!option.DisableUpdateCheck)
|
||||||
|
_ = CheckUpdateAsync();
|
||||||
|
|
||||||
|
Logger.IsWriteFile = !option.NoLog;
|
||||||
|
Logger.InitLogFile();
|
||||||
|
Logger.LogLevel = option.LogLevel;
|
||||||
|
Logger.Info(CommandInvoker.VERSION_INFO);
|
||||||
|
|
||||||
|
if (option.UseSystemProxy == false)
|
||||||
|
{
|
||||||
|
HTTPUtil.HttpClientHandler.UseProxy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task DoWorkAsync(MyOption option)
|
if (option.CustomProxy != null)
|
||||||
{
|
{
|
||||||
|
HTTPUtil.HttpClientHandler.Proxy = option.CustomProxy;
|
||||||
if (Console.IsOutputRedirected || Console.IsErrorRedirected)
|
HTTPUtil.HttpClientHandler.UseProxy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查互斥的选项
|
||||||
|
if (option is { MuxAfterDone: false, MuxImports.Count: > 0 })
|
||||||
|
{
|
||||||
|
throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.UseShakaPackager)
|
||||||
|
{
|
||||||
|
option.DecryptionEngine = DecryptEngine.SHAKA_PACKAGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LivePipeMux开启时 LiveRealTimeMerge必须开启
|
||||||
|
if (option is { LivePipeMux: true, LiveRealTimeMerge: false })
|
||||||
|
{
|
||||||
|
Logger.WarnMarkUp("LivePipeMux detected, forced enable LiveRealTimeMerge");
|
||||||
|
option.LiveRealTimeMerge = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预先检查ffmpeg
|
||||||
|
option.FFmpegBinaryPath ??= GlobalUtil.FindExecutable("ffmpeg");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(option.FFmpegBinaryPath) || !File.Exists(option.FFmpegBinaryPath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException(ResString.ffmpegNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Extra($"ffmpeg => {option.FFmpegBinaryPath}");
|
||||||
|
|
||||||
|
// 预先检查mkvmerge
|
||||||
|
if (option is { MuxOptions.UseMkvmerge: true, MuxAfterDone: true })
|
||||||
|
{
|
||||||
|
option.MkvmergeBinaryPath ??= GlobalUtil.FindExecutable("mkvmerge");
|
||||||
|
if (string.IsNullOrEmpty(option.MkvmergeBinaryPath) || !File.Exists(option.MkvmergeBinaryPath))
|
||||||
{
|
{
|
||||||
option.ForceAnsiConsole = true;
|
throw new FileNotFoundException(ResString.mkvmergeNotFound);
|
||||||
option.NoAnsiColor = true;
|
|
||||||
Logger.Info(ResString.consoleRedirected);
|
|
||||||
}
|
}
|
||||||
CustomAnsiConsole.InitConsole(option.ForceAnsiConsole, option.NoAnsiColor);
|
Logger.Extra($"mkvmerge => {option.MkvmergeBinaryPath}");
|
||||||
//检测更新
|
}
|
||||||
CheckUpdateAsync();
|
|
||||||
|
|
||||||
Logger.IsWriteFile = !option.NoLog;
|
// 预先检查
|
||||||
Logger.InitLogFile();
|
if (option.Keys is { Length: > 0 } || option.KeyTextFile != null)
|
||||||
Logger.LogLevel = option.LogLevel;
|
{
|
||||||
Logger.Info(CommandInvoker.VERSION_INFO);
|
if (!string.IsNullOrEmpty(option.DecryptionBinaryPath) && !File.Exists(option.DecryptionBinaryPath))
|
||||||
|
|
||||||
if (option.UseSystemProxy == false)
|
|
||||||
{
|
{
|
||||||
HTTPUtil.HttpClientHandler.UseProxy = false;
|
throw new FileNotFoundException(option.DecryptionBinaryPath);
|
||||||
}
|
}
|
||||||
|
switch (option.DecryptionEngine)
|
||||||
if (option.CustomProxy != null)
|
|
||||||
{
|
{
|
||||||
HTTPUtil.HttpClientHandler.Proxy = option.CustomProxy;
|
case DecryptEngine.SHAKA_PACKAGER:
|
||||||
HTTPUtil.HttpClientHandler.UseProxy = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//检查互斥的选项
|
|
||||||
|
|
||||||
if (!option.MuxAfterDone && option.MuxImports != null && option.MuxImports.Count > 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("MuxAfterDone disabled, MuxImports not allowed!");
|
|
||||||
}
|
|
||||||
|
|
||||||
//LivePipeMux开启时 LiveRealTimeMerge必须开启
|
|
||||||
if (option.LivePipeMux && !option.LiveRealTimeMerge)
|
|
||||||
{
|
|
||||||
Logger.WarnMarkUp("LivePipeMux detected, forced enable LiveRealTimeMerge");
|
|
||||||
option.LiveRealTimeMerge = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//预先检查ffmpeg
|
|
||||||
if (option.FFmpegBinaryPath == null)
|
|
||||||
option.FFmpegBinaryPath = GlobalUtil.FindExecutable("ffmpeg");
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(option.FFmpegBinaryPath) || !File.Exists(option.FFmpegBinaryPath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException(ResString.ffmpegNotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Extra($"ffmpeg => {option.FFmpegBinaryPath}");
|
|
||||||
|
|
||||||
//预先检查mkvmerge
|
|
||||||
if (option.MuxOptions != null && option.MuxOptions.UseMkvmerge && option.MuxAfterDone)
|
|
||||||
{
|
|
||||||
if (option.MkvmergeBinaryPath == null)
|
|
||||||
option.MkvmergeBinaryPath = GlobalUtil.FindExecutable("mkvmerge");
|
|
||||||
if (string.IsNullOrEmpty(option.MkvmergeBinaryPath) || !File.Exists(option.MkvmergeBinaryPath))
|
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException("mkvmerge not found");
|
var file = GlobalUtil.FindExecutable("shaka-packager");
|
||||||
|
var file2 = GlobalUtil.FindExecutable("packager-linux-x64");
|
||||||
|
var file3 = GlobalUtil.FindExecutable("packager-osx-x64");
|
||||||
|
var file4 = GlobalUtil.FindExecutable("packager-win-x64");
|
||||||
|
if (file == null && file2 == null && file3 == null && file4 == null)
|
||||||
|
throw new FileNotFoundException(ResString.shakaPackagerNotFound);
|
||||||
|
option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
|
||||||
|
Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Logger.Extra($"mkvmerge => {option.MkvmergeBinaryPath}");
|
case DecryptEngine.MP4DECRYPT:
|
||||||
}
|
|
||||||
|
|
||||||
//预先检查
|
|
||||||
if ((option.Keys != null && option.Keys.Length > 0) || option.KeyTextFile != null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(option.DecryptionBinaryPath))
|
|
||||||
{
|
{
|
||||||
if (option.UseShakaPackager)
|
var file = GlobalUtil.FindExecutable("mp4decrypt");
|
||||||
{
|
if (file == null) throw new FileNotFoundException(ResString.mp4decryptNotFound);
|
||||||
var file = GlobalUtil.FindExecutable("shaka-packager");
|
option.DecryptionBinaryPath = file;
|
||||||
var file2 = GlobalUtil.FindExecutable("packager-linux-x64");
|
Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}");
|
||||||
var file3 = GlobalUtil.FindExecutable("packager-osx-x64");
|
break;
|
||||||
var file4 = GlobalUtil.FindExecutable("packager-win-x64");
|
|
||||||
if (file == null && file2 == null && file3 == null && file4 == null) throw new FileNotFoundException("shaka-packager not found!");
|
|
||||||
option.DecryptionBinaryPath = file ?? file2 ?? file3 ?? file4;
|
|
||||||
Logger.Extra($"shaka-packager => {option.DecryptionBinaryPath}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var file = GlobalUtil.FindExecutable("mp4decrypt");
|
|
||||||
if (file == null) throw new FileNotFoundException("mp4decrypt not found!");
|
|
||||||
option.DecryptionBinaryPath = file;
|
|
||||||
Logger.Extra($"mp4decrypt => {option.DecryptionBinaryPath}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!File.Exists(option.DecryptionBinaryPath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException(option.DecryptionBinaryPath);
|
|
||||||
}
|
}
|
||||||
|
case DecryptEngine.FFMPEG:
|
||||||
|
default:
|
||||||
|
option.DecryptionBinaryPath = option.FFmpegBinaryPath;
|
||||||
|
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"
|
||||||
|
};
|
||||||
|
// 添加或替换用户输入的headers
|
||||||
|
foreach (var item in option.Headers)
|
||||||
|
{
|
||||||
|
headers[item.Key] = item.Value;
|
||||||
|
Logger.Extra($"User-Defined Header => {item.Key}: {item.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parserConfig = new ParserConfig()
|
||||||
|
{
|
||||||
|
AppendUrlParams = option.AppendUrlParams,
|
||||||
|
UrlProcessorArgs = option.UrlProcessorArgs,
|
||||||
|
BaseUrl = option.BaseUrl!,
|
||||||
|
Headers = headers,
|
||||||
|
CustomMethod = option.CustomHLSMethod,
|
||||||
|
CustomeKey = option.CustomHLSKey,
|
||||||
|
CustomeIV = option.CustomHLSIv,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (option.AllowHlsMultiExtMap)
|
||||||
|
{
|
||||||
|
parserConfig.CustomParserArgs.Add("AllowHlsMultiExtMap", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
// demo1
|
||||||
|
parserConfig.ContentProcessors.Insert(0, new DemoProcessor());
|
||||||
|
// demo2
|
||||||
|
parserConfig.KeyProcessors.Insert(0, new DemoProcessor2());
|
||||||
|
// for www.nowehoryzonty.pl
|
||||||
|
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
|
||||||
|
|
||||||
|
// 等待任务开始时间
|
||||||
|
if (option.TaskStartAt != null && option.TaskStartAt > DateTime.Now)
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp(ResString.taskStartAt + option.TaskStartAt);
|
||||||
|
while (option.TaskStartAt > DateTime.Now)
|
||||||
{
|
{
|
||||||
["user-agent"] = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
|
await Task.Delay(1000);
|
||||||
};
|
|
||||||
//添加或替换用户输入的headers
|
|
||||||
foreach (var item in option.Headers)
|
|
||||||
{
|
|
||||||
headers[item.Key] = item.Value;
|
|
||||||
Logger.Extra($"User-Defined Header => {item.Key}: {item.Value}");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var parserConfig = new ParserConfig()
|
var url = option.Input;
|
||||||
|
|
||||||
|
// 流提取器配置
|
||||||
|
var extractor = new StreamExtractor(parserConfig);
|
||||||
|
// 从链接加载内容
|
||||||
|
await RetryUtil.WebRequestRetryAsync(async () =>
|
||||||
|
{
|
||||||
|
await extractor.LoadSourceFromUrlAsync(url);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
// 解析流信息
|
||||||
|
var streams = await extractor.ExtractStreamsAsync();
|
||||||
|
|
||||||
|
|
||||||
|
// 全部媒体
|
||||||
|
var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder).ToList();
|
||||||
|
// 基本流
|
||||||
|
var basicStreams = lists.Where(x => x.MediaType is null or MediaType.VIDEO).ToList();
|
||||||
|
// 可选音频轨道
|
||||||
|
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO).ToList();
|
||||||
|
// 可选字幕轨道
|
||||||
|
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES).ToList();
|
||||||
|
|
||||||
|
// 尝试从URL或文件读取文件名
|
||||||
|
if (string.IsNullOrEmpty(option.SaveName))
|
||||||
|
{
|
||||||
|
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")}");
|
||||||
|
// 记录文件
|
||||||
|
extractor.RawFiles["meta.json"] = GlobalUtil.ConvertToJson(lists);
|
||||||
|
// 写出文件
|
||||||
|
await WriteRawFilesAsync(option, extractor, tmpDir);
|
||||||
|
|
||||||
|
Logger.Info(ResString.streamsInfo, lists.Count, basicStreams.Count, audios.Count, subs.Count);
|
||||||
|
|
||||||
|
foreach (var item in lists)
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp(item.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedStreams = new List<StreamSpec>();
|
||||||
|
if (option.DropVideoFilter != null || option.DropAudioFilter != null || option.DropSubtitleFilter != null)
|
||||||
|
{
|
||||||
|
basicStreams = FilterUtil.DoFilterDrop(basicStreams, option.DropVideoFilter);
|
||||||
|
audios = FilterUtil.DoFilterDrop(audios, option.DropAudioFilter);
|
||||||
|
subs = FilterUtil.DoFilterDrop(subs, option.DropSubtitleFilter);
|
||||||
|
lists = basicStreams.Concat(audios).Concat(subs).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.DropVideoFilter != null) Logger.Extra($"DropVideoFilter => {option.DropVideoFilter}");
|
||||||
|
if (option.DropAudioFilter != null) Logger.Extra($"DropAudioFilter => {option.DropAudioFilter}");
|
||||||
|
if (option.DropSubtitleFilter != null) Logger.Extra($"DropSubtitleFilter => {option.DropSubtitleFilter}");
|
||||||
|
if (option.VideoFilter != null) Logger.Extra($"VideoFilter => {option.VideoFilter}");
|
||||||
|
if (option.AudioFilter != null) Logger.Extra($"AudioFilter => {option.AudioFilter}");
|
||||||
|
if (option.SubtitleFilter != null) Logger.Extra($"SubtitleFilter => {option.SubtitleFilter}");
|
||||||
|
|
||||||
|
if (option.AutoSelect)
|
||||||
|
{
|
||||||
|
if (basicStreams.Count != 0)
|
||||||
|
selectedStreams.Add(basicStreams.First());
|
||||||
|
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
|
||||||
|
foreach (var lang in langs)
|
||||||
{
|
{
|
||||||
AppendUrlParams = option.AppendUrlParams,
|
selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).ThenByDescending(GetOrder).First());
|
||||||
UrlProcessorArgs = option.UrlProcessorArgs,
|
|
||||||
BaseUrl = option.BaseUrl!,
|
|
||||||
Headers = headers,
|
|
||||||
CustomMethod = option.CustomHLSMethod,
|
|
||||||
CustomeKey = option.CustomHLSKey,
|
|
||||||
CustomeIV = option.CustomHLSIv,
|
|
||||||
};
|
|
||||||
|
|
||||||
//demo1
|
|
||||||
parserConfig.ContentProcessors.Insert(0, new DemoProcessor());
|
|
||||||
//demo2
|
|
||||||
parserConfig.KeyProcessors.Insert(0, new DemoProcessor2());
|
|
||||||
//for www.nowehoryzonty.pl
|
|
||||||
parserConfig.UrlProcessors.Insert(0, new NowehoryzontyUrlProcessor());
|
|
||||||
|
|
||||||
//等待任务开始时间
|
|
||||||
if (option.TaskStartAt != null && option.TaskStartAt > DateTime.Now)
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp(ResString.taskStartAt + option.TaskStartAt);
|
|
||||||
while (option.TaskStartAt > DateTime.Now)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
selectedStreams.AddRange(subs);
|
||||||
|
}
|
||||||
|
else if (option.SubOnly)
|
||||||
|
{
|
||||||
|
selectedStreams.AddRange(subs);
|
||||||
|
}
|
||||||
|
else if (option.VideoFilter != null || option.AudioFilter != null || option.SubtitleFilter != null)
|
||||||
|
{
|
||||||
|
basicStreams = FilterUtil.DoFilterKeep(basicStreams, option.VideoFilter);
|
||||||
|
audios = FilterUtil.DoFilterKeep(audios, option.AudioFilter);
|
||||||
|
subs = FilterUtil.DoFilterKeep(subs, option.SubtitleFilter);
|
||||||
|
selectedStreams = basicStreams.Concat(audios).Concat(subs).ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 展示交互式选择框
|
||||||
|
selectedStreams = FilterUtil.SelectStreams(lists);
|
||||||
|
}
|
||||||
|
|
||||||
var url = option.Input;
|
if (selectedStreams.Count == 0)
|
||||||
|
throw new Exception(ResString.noStreamsToDownload);
|
||||||
|
|
||||||
//流提取器配置
|
// HLS: 选中流中若有没加载出playlist的,加载playlist
|
||||||
var extractor = new StreamExtractor(parserConfig);
|
// DASH/MSS: 加载playlist (调用url预处理器)
|
||||||
// 从链接加载内容
|
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS)
|
||||||
await RetryUtil.WebRequestRetryAsync(async () =>
|
await extractor.FetchPlayListAsync(selectedStreams);
|
||||||
{
|
|
||||||
await extractor.LoadSourceFromUrlAsync(url);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
//解析流信息
|
|
||||||
var streams = await extractor.ExtractStreamsAsync();
|
|
||||||
|
|
||||||
|
// 直播检测
|
||||||
|
var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod;
|
||||||
|
if (livingFlag)
|
||||||
|
{
|
||||||
|
Logger.WarnMarkUp($"[white on darkorange3_1]{ResString.liveFound}[/]");
|
||||||
|
}
|
||||||
|
|
||||||
//全部媒体
|
// 无法识别的加密方式,自动开启二进制合并
|
||||||
var lists = streams.OrderBy(p => p.MediaType).ThenByDescending(p => p.Bandwidth).ThenByDescending(GetOrder);
|
if (selectedStreams.Any(s => s.Playlist!.MediaParts.Any(p => p.MediaSegments.Any(m => m.EncryptInfo.Method == EncryptMethod.UNKNOWN))))
|
||||||
//基本流
|
{
|
||||||
var basicStreams = lists.Where(x => x.MediaType == null || x.MediaType == MediaType.VIDEO);
|
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]");
|
||||||
//可选音频轨道
|
option.BinaryMerge = true;
|
||||||
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
|
}
|
||||||
//可选字幕轨道
|
|
||||||
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
|
|
||||||
|
|
||||||
//尝试从URL或文件读取文件名
|
// 应用用户自定义的分片范围
|
||||||
if (string.IsNullOrEmpty(option.SaveName))
|
if (!livingFlag)
|
||||||
{
|
FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange);
|
||||||
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")}");
|
FilterUtil.CleanAd(selectedStreams, option.AdKeywords);
|
||||||
//记录文件
|
|
||||||
extractor.RawFiles["meta.json"] = GlobalUtil.ConvertToJson(lists);
|
|
||||||
//写出文件
|
|
||||||
await WriteRawFilesAsync(option, extractor, tmpDir);
|
|
||||||
|
|
||||||
Logger.Info(ResString.streamsInfo, lists.Count(), basicStreams.Count(), audios.Count(), subs.Count());
|
// 记录文件
|
||||||
|
extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams);
|
||||||
|
|
||||||
foreach (var item in lists)
|
Logger.Info(ResString.selectedStream);
|
||||||
{
|
foreach (var item in selectedStreams)
|
||||||
Logger.InfoMarkUp(item.ToString());
|
{
|
||||||
}
|
Logger.InfoMarkUp(item.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
var selectedStreams = new List<StreamSpec>();
|
// 写出文件
|
||||||
if (option.DropVideoFilter != null || option.DropAudioFilter != null || option.DropSubtitleFilter != null)
|
await WriteRawFilesAsync(option, extractor, tmpDir);
|
||||||
{
|
|
||||||
basicStreams = FilterUtil.DoFilterDrop(basicStreams, option.DropVideoFilter);
|
|
||||||
audios = FilterUtil.DoFilterDrop(audios, option.DropAudioFilter);
|
|
||||||
subs = FilterUtil.DoFilterDrop(subs, option.DropSubtitleFilter);
|
|
||||||
lists = basicStreams.Concat(audios).Concat(subs).OrderBy(x => true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (option.DropVideoFilter != null) Logger.Extra($"DropVideoFilter => {option.DropVideoFilter}");
|
if (option.SkipDownload)
|
||||||
if (option.DropAudioFilter != null) Logger.Extra($"DropAudioFilter => {option.DropAudioFilter}");
|
{
|
||||||
if (option.DropSubtitleFilter != null) Logger.Extra($"DropSubtitleFilter => {option.DropSubtitleFilter}");
|
return;
|
||||||
if (option.VideoFilter != null) Logger.Extra($"VideoFilter => {option.VideoFilter}");
|
}
|
||||||
if (option.AudioFilter != null) Logger.Extra($"AudioFilter => {option.AudioFilter}");
|
|
||||||
if (option.SubtitleFilter != null) Logger.Extra($"SubtitleFilter => {option.SubtitleFilter}");
|
|
||||||
|
|
||||||
if (option.AutoSelect)
|
|
||||||
{
|
|
||||||
if (basicStreams.Any())
|
|
||||||
selectedStreams.Add(basicStreams.First());
|
|
||||||
var langs = audios.DistinctBy(a => a.Language).Select(a => a.Language);
|
|
||||||
foreach (var lang in langs)
|
|
||||||
{
|
|
||||||
selectedStreams.Add(audios.Where(a => a.Language == lang).OrderByDescending(a => a.Bandwidth).ThenByDescending(GetOrder).First());
|
|
||||||
}
|
|
||||||
selectedStreams.AddRange(subs);
|
|
||||||
}
|
|
||||||
else if (option.SubOnly)
|
|
||||||
{
|
|
||||||
selectedStreams.AddRange(subs);
|
|
||||||
}
|
|
||||||
else if (option.VideoFilter != null || option.AudioFilter != null || option.SubtitleFilter != null)
|
|
||||||
{
|
|
||||||
basicStreams = FilterUtil.DoFilterKeep(basicStreams, option.VideoFilter);
|
|
||||||
audios = FilterUtil.DoFilterKeep(audios, option.AudioFilter);
|
|
||||||
subs = FilterUtil.DoFilterKeep(subs, option.SubtitleFilter);
|
|
||||||
selectedStreams = basicStreams.Concat(audios).Concat(subs).ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//展示交互式选择框
|
|
||||||
selectedStreams = FilterUtil.SelectStreams(lists);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedStreams.Any())
|
|
||||||
throw new Exception(ResString.noStreamsToDownload);
|
|
||||||
|
|
||||||
//HLS: 选中流中若有没加载出playlist的,加载playlist
|
|
||||||
//DASH/MSS: 加载playlist (调用url预处理器)
|
|
||||||
if (selectedStreams.Any(s => s.Playlist == null) || extractor.ExtractorType == ExtractorType.MPEG_DASH || extractor.ExtractorType == ExtractorType.MSS)
|
|
||||||
await extractor.FetchPlayListAsync(selectedStreams);
|
|
||||||
|
|
||||||
//直播检测
|
|
||||||
var livingFlag = selectedStreams.Any(s => s.Playlist?.IsLive == true) && !option.LivePerformAsVod;
|
|
||||||
if (livingFlag)
|
|
||||||
{
|
|
||||||
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))))
|
|
||||||
{
|
|
||||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.autoBinaryMerge3}[/]");
|
|
||||||
option.BinaryMerge = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//应用用户自定义的分片范围
|
|
||||||
if (!livingFlag)
|
|
||||||
FilterUtil.ApplyCustomRange(selectedStreams, option.CustomRange);
|
|
||||||
|
|
||||||
//应用用户自定义的广告分片关键字
|
|
||||||
FilterUtil.CleanAd(selectedStreams, option.AdKeywords);
|
|
||||||
|
|
||||||
//记录文件
|
|
||||||
extractor.RawFiles["meta_selected.json"] = GlobalUtil.ConvertToJson(selectedStreams);
|
|
||||||
|
|
||||||
Logger.Info(ResString.selectedStream);
|
|
||||||
foreach (var item in selectedStreams)
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp(item.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
//写出文件
|
|
||||||
await WriteRawFilesAsync(option, extractor, tmpDir);
|
|
||||||
|
|
||||||
if (option.SkipDownload)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine("Press any key to continue...");
|
Console.WriteLine("Press any key to continue...");
|
||||||
Console.ReadKey();
|
Console.ReadKey();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
MyOptions = option,
|
|
||||||
DirPrefix = tmpDir,
|
|
||||||
Headers = parserConfig.Headers, //使用命令行解析得到的Headers
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = false;
|
|
||||||
|
|
||||||
if (extractor.ExtractorType == ExtractorType.HTTP_LIVE)
|
|
||||||
{
|
|
||||||
var sldm = new HTTPLiveRecordManager(downloadConfig, selectedStreams, extractor);
|
|
||||||
result = await sldm.StartRecordAsync();
|
|
||||||
}
|
|
||||||
else if (!livingFlag)
|
|
||||||
{
|
|
||||||
//开始下载
|
|
||||||
var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor);
|
|
||||||
result = await sdm.StartDownloadAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var sldm = new SimpleLiveRecordManager2(downloadConfig, selectedStreams, extractor);
|
|
||||||
result = await sldm.StartRecordAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp("[white on green]Done[/]");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.ErrorMarkUp("[white on red]Failed[/]");
|
|
||||||
Environment.ExitCode = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir)
|
// 下载配置
|
||||||
|
var downloadConfig = new DownloaderConfig()
|
||||||
{
|
{
|
||||||
//写出json文件
|
MyOptions = option,
|
||||||
if (option.WriteMetaJson)
|
DirPrefix = tmpDir,
|
||||||
{
|
Headers = parserConfig.Headers, // 使用命令行解析得到的Headers
|
||||||
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
|
};
|
||||||
Logger.Warn(ResString.writeJson);
|
|
||||||
foreach (var item in extractor.RawFiles)
|
var result = false;
|
||||||
{
|
|
||||||
var file = Path.Combine(tmpDir, item.Key);
|
if (extractor.ExtractorType == ExtractorType.HTTP_LIVE)
|
||||||
if (!File.Exists(file)) await File.WriteAllTextAsync(file, item.Value, Encoding.UTF8);
|
{
|
||||||
}
|
var sldm = new HTTPLiveRecordManager(downloadConfig, selectedStreams, extractor);
|
||||||
}
|
result = await sldm.StartRecordAsync();
|
||||||
|
}
|
||||||
|
else if (!livingFlag)
|
||||||
|
{
|
||||||
|
// 开始下载
|
||||||
|
var sdm = new SimpleDownloadManager(downloadConfig, selectedStreams, extractor);
|
||||||
|
result = await sdm.StartDownloadAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var sldm = new SimpleLiveRecordManager2(downloadConfig, selectedStreams, extractor);
|
||||||
|
result = await sldm.StartRecordAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task CheckUpdateAsync()
|
if (result)
|
||||||
{
|
{
|
||||||
try
|
Logger.InfoMarkUp("[white on green]Done[/]");
|
||||||
{
|
|
||||||
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!;
|
|
||||||
string nowVer = $"v{ver.Major}.{ver.Minor}.{ver.Build}";
|
|
||||||
string redirctUrl = await Get302Async("https://github.com/nilaoda/N_m3u8DL-RE/releases/latest");
|
|
||||||
string latestVer = redirctUrl.Replace("https://github.com/nilaoda/N_m3u8DL-RE/releases/tag/", "");
|
|
||||||
if (!latestVer.StartsWith(nowVer) && !latestVer.StartsWith("https"))
|
|
||||||
{
|
|
||||||
Console.Title = $"{ResString.newVersionFound} {latestVer}";
|
|
||||||
Logger.InfoMarkUp($"[cyan]{ResString.newVersionFound}[/] [red]{latestVer}[/]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
//重定向
|
|
||||||
static async Task<string> Get302Async(string url)
|
|
||||||
{
|
{
|
||||||
//this allows you to set the settings so that we can get the redirect url
|
Logger.ErrorMarkUp("[white on red]Failed[/]");
|
||||||
var handler = new HttpClientHandler()
|
Environment.ExitCode = 1;
|
||||||
{
|
|
||||||
AllowAutoRedirect = false
|
|
||||||
};
|
|
||||||
string redirectedUrl = "";
|
|
||||||
using (HttpClient client = new(handler))
|
|
||||||
using (HttpResponseMessage response = await client.GetAsync(url))
|
|
||||||
using (HttpContent content = response.Content)
|
|
||||||
{
|
|
||||||
// ... Read the response to see if we have the redirected url
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.Found)
|
|
||||||
{
|
|
||||||
HttpResponseHeaders headers = response.Headers;
|
|
||||||
if (headers != null && headers.Location != null)
|
|
||||||
{
|
|
||||||
redirectedUrl = headers.Location.AbsoluteUri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirectedUrl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task WriteRawFilesAsync(MyOption option, StreamExtractor extractor, string tmpDir)
|
||||||
|
{
|
||||||
|
// 写出json文件
|
||||||
|
if (option.WriteMetaJson)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(tmpDir)) Directory.CreateDirectory(tmpDir);
|
||||||
|
Logger.Warn(ResString.writeJson);
|
||||||
|
foreach (var item in extractor.RawFiles)
|
||||||
|
{
|
||||||
|
var file = Path.Combine(tmpDir, item.Key);
|
||||||
|
if (!File.Exists(file)) await File.WriteAllTextAsync(file, item.Value, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task CheckUpdateAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!;
|
||||||
|
string nowVer = $"v{ver.Major}.{ver.Minor}.{ver.Build}";
|
||||||
|
string redirctUrl = await Get302Async("https://github.com/nilaoda/N_m3u8DL-RE/releases/latest");
|
||||||
|
string latestVer = redirctUrl.Replace("https://github.com/nilaoda/N_m3u8DL-RE/releases/tag/", "");
|
||||||
|
if (!latestVer.StartsWith(nowVer) && !latestVer.StartsWith("https"))
|
||||||
|
{
|
||||||
|
Console.Title = $"{ResString.newVersionFound} {latestVer}";
|
||||||
|
Logger.InfoMarkUp($"[cyan]{ResString.newVersionFound}[/] [red]{latestVer}[/]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重定向
|
||||||
|
static async Task<string> Get302Async(string url)
|
||||||
|
{
|
||||||
|
// this allows you to set the settings so that we can get the redirect url
|
||||||
|
var handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = false
|
||||||
|
};
|
||||||
|
var redirectedUrl = "";
|
||||||
|
using var client = new HttpClient(handler);
|
||||||
|
using var response = await client.GetAsync(url);
|
||||||
|
using var content = response.Content;
|
||||||
|
// ... Read the response to see if we have the redirected url
|
||||||
|
if (response.StatusCode != HttpStatusCode.Found) return redirectedUrl;
|
||||||
|
|
||||||
|
var headers = response.Headers;
|
||||||
|
if (headers.Location != null)
|
||||||
|
{
|
||||||
|
redirectedUrl = headers.Location.AbsoluteUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirectedUrl;
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,145 +2,142 @@
|
|||||||
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 class DownloadUtil
|
|
||||||
{
|
|
||||||
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)
|
internal static class DownloadUtil
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
using var inputStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var outputStream = new FileStream(path, FileMode.OpenOrCreate);
|
||||||
|
inputStream.Seek(fromPosition ?? 0L, SeekOrigin.Begin);
|
||||||
|
var expect = (toPosition ?? inputStream.Length) - inputStream.Position + 1;
|
||||||
|
if (expect == inputStream.Length + 1)
|
||||||
{
|
{
|
||||||
using var inputStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read);
|
await inputStream.CopyToAsync(outputStream);
|
||||||
using var outputStream = new FileStream(path, FileMode.OpenOrCreate);
|
speedContainer.Add(inputStream.Length);
|
||||||
inputStream.Seek(fromPosition ?? 0L, SeekOrigin.Begin);
|
}
|
||||||
var expect = (toPosition ?? inputStream.Length) - inputStream.Position + 1;
|
else
|
||||||
if (expect == inputStream.Length + 1)
|
{
|
||||||
{
|
var buffer = new byte[expect];
|
||||||
await inputStream.CopyToAsync(outputStream);
|
_ = await inputStream.ReadAsync(buffer);
|
||||||
speedContainer.Add(inputStream.Length);
|
await outputStream.WriteAsync(buffer);
|
||||||
}
|
speedContainer.Add(buffer.Length);
|
||||||
else
|
}
|
||||||
{
|
return new DownloadResult()
|
||||||
var buffer = new byte[expect];
|
{
|
||||||
await inputStream.ReadAsync(buffer);
|
ActualContentLength = outputStream.Length,
|
||||||
await outputStream.WriteAsync(buffer, 0, buffer.Length);
|
ActualFilePath = path
|
||||||
speedContainer.Add(buffer.Length);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<DownloadResult> DownloadToFileAsync(string url, string path, SpeedContainer speedContainer, CancellationTokenSource cancellationTokenSource, Dictionary<string, string>? headers = null, long? fromPosition = null, long? toPosition = null)
|
||||||
|
{
|
||||||
|
Logger.Debug(ResString.fetch + url);
|
||||||
|
if (url.StartsWith("file:"))
|
||||||
|
{
|
||||||
|
var file = new Uri(url).LocalPath;
|
||||||
|
return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition);
|
||||||
|
}
|
||||||
|
if (url.StartsWith("base64://"))
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(url[9..]);
|
||||||
|
await File.WriteAllBytesAsync(path, bytes);
|
||||||
return new DownloadResult()
|
return new DownloadResult()
|
||||||
{
|
{
|
||||||
ActualContentLength = outputStream.Length,
|
ActualContentLength = bytes.Length,
|
||||||
ActualFilePath = path
|
ActualFilePath = path,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (url.StartsWith("hex://"))
|
||||||
public static async Task<DownloadResult> DownloadToFileAsync(string url, string path, SpeedContainer speedContainer, CancellationTokenSource cancellationTokenSource, Dictionary<string, string>? headers = null, long? fromPosition = null, long? toPosition = null)
|
|
||||||
{
|
{
|
||||||
Logger.Debug(ResString.fetch + url);
|
var bytes = HexUtil.HexToBytes(url[6..]);
|
||||||
if (url.StartsWith("file:"))
|
await File.WriteAllBytesAsync(path, bytes);
|
||||||
|
return new DownloadResult()
|
||||||
{
|
{
|
||||||
var file = new Uri(url).LocalPath;
|
ActualContentLength = bytes.Length,
|
||||||
return await CopyFileAsync(file, path, speedContainer, fromPosition, toPosition);
|
ActualFilePath = path,
|
||||||
}
|
};
|
||||||
if (url.StartsWith("base64://"))
|
}
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
||||||
|
if (fromPosition != null || toPosition != null)
|
||||||
|
request.Headers.Range = new(fromPosition, toPosition);
|
||||||
|
if (headers != null)
|
||||||
|
{
|
||||||
|
foreach (var item in headers)
|
||||||
{
|
{
|
||||||
var bytes = Convert.FromBase64String(url[9..]);
|
request.Headers.TryAddWithoutValidation(item.Key, item.Value);
|
||||||
await File.WriteAllBytesAsync(path, bytes);
|
|
||||||
return new DownloadResult()
|
|
||||||
{
|
|
||||||
ActualContentLength = bytes.Length,
|
|
||||||
ActualFilePath = path,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (url.StartsWith("hex://"))
|
|
||||||
{
|
|
||||||
var bytes = HexUtil.HexToBytes(url[6..]);
|
|
||||||
await File.WriteAllBytesAsync(path, bytes);
|
|
||||||
return new DownloadResult()
|
|
||||||
{
|
|
||||||
ActualContentLength = bytes.Length,
|
|
||||||
ActualFilePath = path,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
|
||||||
if (fromPosition != null || toPosition != null)
|
|
||||||
request.Headers.Range = new(fromPosition, toPosition);
|
|
||||||
if (headers != null)
|
|
||||||
{
|
|
||||||
foreach (var item in headers)
|
|
||||||
{
|
|
||||||
request.Headers.TryAddWithoutValidation(item.Key, item.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logger.Debug(request.Headers.ToString());
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var response = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token);
|
|
||||||
if (((int)response.StatusCode).ToString().StartsWith("30"))
|
|
||||||
{
|
|
||||||
HttpResponseHeaders respHeaders = response.Headers;
|
|
||||||
Logger.Debug(respHeaders.ToString());
|
|
||||||
if (respHeaders != null && respHeaders.Location != null)
|
|
||||||
{
|
|
||||||
var redirectedUrl = "";
|
|
||||||
if (!respHeaders.Location.IsAbsoluteUri)
|
|
||||||
{
|
|
||||||
Uri uri1 = new Uri(url);
|
|
||||||
Uri uri2 = new Uri(uri1, respHeaders.Location);
|
|
||||||
redirectedUrl = uri2.ToString();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
redirectedUrl = respHeaders.Location.AbsoluteUri;
|
|
||||||
}
|
|
||||||
return await DownloadToFileAsync(redirectedUrl, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var contentLength = response.Content.Headers.ContentLength;
|
|
||||||
if (speedContainer.SingleSegment) speedContainer.ResponseLength = contentLength;
|
|
||||||
|
|
||||||
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
||||||
using var responseStream = await response.Content.ReadAsStreamAsync(cancellationTokenSource.Token);
|
|
||||||
var buffer = new byte[16 * 1024];
|
|
||||||
var size = 0;
|
|
||||||
|
|
||||||
size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);
|
|
||||||
speedContainer.Add(size);
|
|
||||||
await stream.WriteAsync(buffer, 0, size);
|
|
||||||
//检测imageHeader
|
|
||||||
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
|
|
||||||
//检测GZip(For DDP Audio)
|
|
||||||
bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;
|
|
||||||
|
|
||||||
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
|
|
||||||
{
|
|
||||||
speedContainer.Add(size);
|
|
||||||
await stream.WriteAsync(buffer, 0, size);
|
|
||||||
//限速策略
|
|
||||||
while (speedContainer.Downloaded > speedContainer.SpeedLimit)
|
|
||||||
{
|
|
||||||
await Task.Delay(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DownloadResult()
|
|
||||||
{
|
|
||||||
ActualContentLength = stream.Length,
|
|
||||||
RespContentLength = contentLength,
|
|
||||||
ActualFilePath = path,
|
|
||||||
ImageHeader= imageHeader,
|
|
||||||
GzipHeader = gZipHeader
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationTokenSource.Token)
|
|
||||||
{
|
|
||||||
speedContainer.ResetLowSpeedCount();
|
|
||||||
throw new Exception("Download speed too slow!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Logger.Debug(request.Headers.ToString());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationTokenSource.Token);
|
||||||
|
if (((int)response.StatusCode).ToString().StartsWith("30"))
|
||||||
|
{
|
||||||
|
HttpResponseHeaders respHeaders = response.Headers;
|
||||||
|
Logger.Debug(respHeaders.ToString());
|
||||||
|
if (respHeaders.Location != null)
|
||||||
|
{
|
||||||
|
var redirectedUrl = "";
|
||||||
|
if (!respHeaders.Location.IsAbsoluteUri)
|
||||||
|
{
|
||||||
|
Uri uri1 = new Uri(url);
|
||||||
|
Uri uri2 = new Uri(uri1, respHeaders.Location);
|
||||||
|
redirectedUrl = uri2.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
redirectedUrl = respHeaders.Location.AbsoluteUri;
|
||||||
|
}
|
||||||
|
return await DownloadToFileAsync(redirectedUrl, path, speedContainer, cancellationTokenSource, headers, fromPosition, toPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var contentLength = response.Content.Headers.ContentLength;
|
||||||
|
if (speedContainer.SingleSegment) speedContainer.ResponseLength = contentLength;
|
||||||
|
|
||||||
|
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
using var responseStream = await response.Content.ReadAsStreamAsync(cancellationTokenSource.Token);
|
||||||
|
var buffer = new byte[16 * 1024];
|
||||||
|
var size = 0;
|
||||||
|
|
||||||
|
size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token);
|
||||||
|
speedContainer.Add(size);
|
||||||
|
await stream.WriteAsync(buffer.AsMemory(0, size));
|
||||||
|
// 检测imageHeader
|
||||||
|
bool imageHeader = ImageHeaderUtil.IsImageHeader(buffer);
|
||||||
|
// 检测GZip(For DDP Audio)
|
||||||
|
bool gZipHeader = buffer.Length > 2 && buffer[0] == 0x1f && buffer[1] == 0x8b;
|
||||||
|
|
||||||
|
while ((size = await responseStream.ReadAsync(buffer, cancellationTokenSource.Token)) > 0)
|
||||||
|
{
|
||||||
|
speedContainer.Add(size);
|
||||||
|
await stream.WriteAsync(buffer.AsMemory(0, size));
|
||||||
|
// 限速策略
|
||||||
|
while (speedContainer.Downloaded > speedContainer.SpeedLimit)
|
||||||
|
{
|
||||||
|
await Task.Delay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DownloadResult()
|
||||||
|
{
|
||||||
|
ActualContentLength = stream.Length,
|
||||||
|
RespContentLength = contentLength,
|
||||||
|
ActualFilePath = path,
|
||||||
|
ImageHeader= imageHeader,
|
||||||
|
GzipHeader = gZipHeader
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationTokenSource.Token)
|
||||||
|
{
|
||||||
|
speedContainer.ResetLowSpeedCount();
|
||||||
|
throw new Exception("Download speed too slow!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,281 +4,270 @@ 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 [];
|
||||||
|
|
||||||
|
var inputs = lists.Where(_ => true);
|
||||||
|
if (filter.GroupIdReg != null)
|
||||||
|
inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId));
|
||||||
|
if (filter.LanguageReg != null)
|
||||||
|
inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language));
|
||||||
|
if (filter.NameReg != null)
|
||||||
|
inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name));
|
||||||
|
if (filter.CodecsReg != null)
|
||||||
|
inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs));
|
||||||
|
if (filter.ResolutionReg != null)
|
||||||
|
inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution));
|
||||||
|
if (filter.FrameRateReg != null)
|
||||||
|
inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($"{i.FrameRate}"));
|
||||||
|
if (filter.ChannelsReg != null)
|
||||||
|
inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels));
|
||||||
|
if (filter.VideoRangeReg != null)
|
||||||
|
inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange));
|
||||||
|
if (filter.UrlReg != null)
|
||||||
|
inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url));
|
||||||
|
if (filter.SegmentsMaxCount != null && inputs.All(i => i.SegmentsCount > 0))
|
||||||
|
inputs = inputs.Where(i => i.SegmentsCount < filter.SegmentsMaxCount);
|
||||||
|
if (filter.SegmentsMinCount != null && inputs.All(i => i.SegmentsCount > 0))
|
||||||
|
inputs = inputs.Where(i => i.SegmentsCount > filter.SegmentsMinCount);
|
||||||
|
if (filter.PlaylistMinDur != null)
|
||||||
|
inputs = inputs.Where(i => i.Playlist?.TotalDuration > filter.PlaylistMinDur);
|
||||||
|
if (filter.PlaylistMaxDur != null)
|
||||||
|
inputs = inputs.Where(i => i.Playlist?.TotalDuration < filter.PlaylistMaxDur);
|
||||||
|
if (filter.BandwidthMin != null)
|
||||||
|
inputs = inputs.Where(i => i.Bandwidth >= filter.BandwidthMin);
|
||||||
|
if (filter.BandwidthMax != null)
|
||||||
|
inputs = inputs.Where(i => i.Bandwidth <= filter.BandwidthMax);
|
||||||
|
if (filter.Role.HasValue)
|
||||||
|
inputs = inputs.Where(i => i.Role == filter.Role);
|
||||||
|
|
||||||
|
var bestNumberStr = filter.For.Replace("best", "");
|
||||||
|
var worstNumberStr = filter.For.Replace("worst", "");
|
||||||
|
|
||||||
|
if (filter.For == "best" && inputs.Any())
|
||||||
|
inputs = inputs.Take(1).ToList();
|
||||||
|
else if (filter.For == "worst" && inputs.Any())
|
||||||
|
inputs = inputs.TakeLast(1).ToList();
|
||||||
|
else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Any())
|
||||||
|
inputs = inputs.Take(bestNumber).ToList();
|
||||||
|
else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Any())
|
||||||
|
inputs = inputs.TakeLast(worstNumber).ToList();
|
||||||
|
|
||||||
|
return inputs.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)
|
||||||
|
{
|
||||||
|
if (filter == null) return [..lists];
|
||||||
|
|
||||||
|
var inputs = lists.Where(_ => true);
|
||||||
|
var selected = DoFilterKeep(lists, filter);
|
||||||
|
|
||||||
|
inputs = inputs.Where(i => selected.All(s => s.ToString() != i.ToString()));
|
||||||
|
|
||||||
|
return inputs.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
|
||||||
|
{
|
||||||
|
var streamSpecs = lists.ToList();
|
||||||
|
if (streamSpecs.Count == 1)
|
||||||
|
return [..streamSpecs];
|
||||||
|
|
||||||
|
// 基本流
|
||||||
|
var basicStreams = streamSpecs.Where(x => x.MediaType == null).ToList();
|
||||||
|
// 可选音频轨道
|
||||||
|
var audios = streamSpecs.Where(x => x.MediaType == MediaType.AUDIO).ToList();
|
||||||
|
// 可选字幕轨道
|
||||||
|
var subs = streamSpecs.Where(x => x.MediaType == MediaType.SUBTITLES).ToList();
|
||||||
|
|
||||||
|
var prompt = new MultiSelectionPrompt<StreamSpec>()
|
||||||
|
.Title(ResString.promptTitle)
|
||||||
|
.UseConverter(x =>
|
||||||
|
{
|
||||||
|
if (x.Name != null && x.Name.StartsWith("__"))
|
||||||
|
return $"[darkslategray1]{x.Name[2..]}[/]";
|
||||||
|
return x.ToString().EscapeMarkup().RemoveMarkup();
|
||||||
|
})
|
||||||
|
.Required()
|
||||||
|
.PageSize(10)
|
||||||
|
.MoreChoicesText(ResString.promptChoiceText)
|
||||||
|
.InstructionsText(ResString.promptInfo)
|
||||||
|
;
|
||||||
|
|
||||||
|
// 默认选中第一个
|
||||||
|
var first = streamSpecs.First();
|
||||||
|
prompt.Select(first);
|
||||||
|
|
||||||
|
if (basicStreams.Count != 0)
|
||||||
{
|
{
|
||||||
if (filter == null) return new List<StreamSpec>();
|
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams);
|
||||||
|
|
||||||
var inputs = lists.Where(_ => true);
|
|
||||||
if (filter.GroupIdReg != null)
|
|
||||||
inputs = inputs.Where(i => i.GroupId != null && filter.GroupIdReg.IsMatch(i.GroupId));
|
|
||||||
if (filter.LanguageReg != null)
|
|
||||||
inputs = inputs.Where(i => i.Language != null && filter.LanguageReg.IsMatch(i.Language));
|
|
||||||
if (filter.NameReg != null)
|
|
||||||
inputs = inputs.Where(i => i.Name != null && filter.NameReg.IsMatch(i.Name));
|
|
||||||
if (filter.CodecsReg != null)
|
|
||||||
inputs = inputs.Where(i => i.Codecs != null && filter.CodecsReg.IsMatch(i.Codecs));
|
|
||||||
if (filter.ResolutionReg != null)
|
|
||||||
inputs = inputs.Where(i => i.Resolution != null && filter.ResolutionReg.IsMatch(i.Resolution));
|
|
||||||
if (filter.FrameRateReg != null)
|
|
||||||
inputs = inputs.Where(i => i.FrameRate != null && filter.FrameRateReg.IsMatch($"{i.FrameRate}"));
|
|
||||||
if (filter.ChannelsReg != null)
|
|
||||||
inputs = inputs.Where(i => i.Channels != null && filter.ChannelsReg.IsMatch(i.Channels));
|
|
||||||
if (filter.VideoRangeReg != null)
|
|
||||||
inputs = inputs.Where(i => i.VideoRange != null && filter.VideoRangeReg.IsMatch(i.VideoRange));
|
|
||||||
if (filter.UrlReg != null)
|
|
||||||
inputs = inputs.Where(i => i.Url != null && filter.UrlReg.IsMatch(i.Url));
|
|
||||||
if (filter.SegmentsMaxCount != null && inputs.All(i => i.SegmentsCount > 0))
|
|
||||||
inputs = inputs.Where(i => i.SegmentsCount < filter.SegmentsMaxCount);
|
|
||||||
if (filter.SegmentsMinCount != null && inputs.All(i => i.SegmentsCount > 0))
|
|
||||||
inputs = inputs.Where(i => i.SegmentsCount > filter.SegmentsMinCount);
|
|
||||||
if (filter.PlaylistMinDur != null)
|
|
||||||
inputs = inputs.Where(i => i.Playlist?.TotalDuration > filter.PlaylistMinDur);
|
|
||||||
if (filter.PlaylistMaxDur != null)
|
|
||||||
inputs = inputs.Where(i => i.Playlist?.TotalDuration < filter.PlaylistMaxDur);
|
|
||||||
if (filter.BandwidthMin != null)
|
|
||||||
inputs = inputs.Where(i => i.Bandwidth >= filter.BandwidthMin);
|
|
||||||
if (filter.BandwidthMax != null)
|
|
||||||
inputs = inputs.Where(i => i.Bandwidth <= filter.BandwidthMax);
|
|
||||||
if (filter.Role.HasValue)
|
|
||||||
inputs = inputs.Where(i => i.Role == filter.Role);
|
|
||||||
|
|
||||||
var bestNumberStr = filter.For.Replace("best", "");
|
|
||||||
var worstNumberStr = filter.For.Replace("worst", "");
|
|
||||||
|
|
||||||
if (filter.For == "best" && inputs.Count() > 0)
|
|
||||||
inputs = inputs.Take(1).ToList();
|
|
||||||
else if (filter.For == "worst" && inputs.Count() > 0)
|
|
||||||
inputs = inputs.TakeLast(1).ToList();
|
|
||||||
else if (int.TryParse(bestNumberStr, out int bestNumber) && inputs.Count() > 0)
|
|
||||||
inputs = inputs.Take(bestNumber).ToList();
|
|
||||||
else if (int.TryParse(worstNumberStr, out int worstNumber) && inputs.Count() > 0)
|
|
||||||
inputs = inputs.TakeLast(worstNumber).ToList();
|
|
||||||
|
|
||||||
return inputs.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<StreamSpec> DoFilterDrop(IEnumerable<StreamSpec> lists, StreamFilter? filter)
|
if (audios.Count != 0)
|
||||||
{
|
{
|
||||||
if (filter == null) return new List<StreamSpec>(lists);
|
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios);
|
||||||
|
// 默认音轨
|
||||||
var inputs = lists.Where(_ => true);
|
if (first.AudioId != null)
|
||||||
var selected = DoFilterKeep(lists, filter);
|
{
|
||||||
|
prompt.Select(audios.First(a => a.GroupId == first.AudioId));
|
||||||
inputs = inputs.Where(i => selected.All(s => s.ToString() != i.ToString()));
|
}
|
||||||
|
}
|
||||||
return inputs.ToList();
|
if (subs.Count != 0)
|
||||||
|
{
|
||||||
|
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs);
|
||||||
|
// 默认字幕轨
|
||||||
|
if (first.SubtitleId != null)
|
||||||
|
{
|
||||||
|
prompt.Select(subs.First(s => s.GroupId == first.SubtitleId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<StreamSpec> SelectStreams(IEnumerable<StreamSpec> lists)
|
// 如果此时还是没有选中任何流,自动选择一个
|
||||||
|
prompt.Select(basicStreams.Concat(audios).Concat(subs).First());
|
||||||
|
|
||||||
|
// 多选
|
||||||
|
var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt);
|
||||||
|
|
||||||
|
return selectedStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直播使用。对齐各个轨道的起始。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="selectedSteams"></param>
|
||||||
|
/// <param name="takeLastCount"></param>
|
||||||
|
public static void SyncStreams(List<StreamSpec> selectedSteams, int takeLastCount = 15)
|
||||||
|
{
|
||||||
|
// 通过Date同步
|
||||||
|
if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null)))
|
||||||
{
|
{
|
||||||
if (lists.Count() == 1)
|
var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!;
|
||||||
return new List<StreamSpec>(lists);
|
foreach (var item in selectedSteams)
|
||||||
|
|
||||||
//基本流
|
|
||||||
var basicStreams = lists.Where(x => x.MediaType == null);
|
|
||||||
//可选音频轨道
|
|
||||||
var audios = lists.Where(x => x.MediaType == MediaType.AUDIO);
|
|
||||||
//可选字幕轨道
|
|
||||||
var subs = lists.Where(x => x.MediaType == MediaType.SUBTITLES);
|
|
||||||
|
|
||||||
var prompt = new MultiSelectionPrompt<StreamSpec>()
|
|
||||||
.Title(ResString.promptTitle)
|
|
||||||
.UseConverter(x =>
|
|
||||||
{
|
|
||||||
if (x.Name != null && x.Name.StartsWith("__"))
|
|
||||||
return $"[darkslategray1]{x.Name.Substring(2)}[/]";
|
|
||||||
else
|
|
||||||
return x.ToString().EscapeMarkup().RemoveMarkup();
|
|
||||||
})
|
|
||||||
.Required()
|
|
||||||
.PageSize(10)
|
|
||||||
.MoreChoicesText(ResString.promptChoiceText)
|
|
||||||
.InstructionsText(ResString.promptInfo)
|
|
||||||
;
|
|
||||||
|
|
||||||
//默认选中第一个
|
|
||||||
var first = lists.First();
|
|
||||||
prompt.Select(first);
|
|
||||||
|
|
||||||
if (basicStreams.Any())
|
|
||||||
{
|
{
|
||||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Basic" }, basicStreams);
|
foreach (var part in item.Playlist!.MediaParts)
|
||||||
}
|
|
||||||
|
|
||||||
if (audios.Any())
|
|
||||||
{
|
|
||||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Audio" }, audios);
|
|
||||||
//默认音轨
|
|
||||||
if (first.AudioId != null)
|
|
||||||
{
|
{
|
||||||
prompt.Select(audios.First(a => a.GroupId == first.AudioId));
|
// 秒级同步 忽略毫秒
|
||||||
|
part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (subs.Any())
|
|
||||||
{
|
|
||||||
prompt.AddChoiceGroup(new StreamSpec() { Name = "__Subtitle" }, subs);
|
|
||||||
//默认字幕轨
|
|
||||||
if (first.SubtitleId != null)
|
|
||||||
{
|
|
||||||
prompt.Select(subs.First(s => s.GroupId == first.SubtitleId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//如果此时还是没有选中任何流,自动选择一个
|
|
||||||
prompt.Select(basicStreams.Concat(audios).Concat(subs).First());
|
|
||||||
|
|
||||||
//多选
|
|
||||||
var selectedStreams = CustomAnsiConsole.Console.Prompt(prompt);
|
|
||||||
|
|
||||||
return selectedStreams;
|
|
||||||
}
|
}
|
||||||
|
else // 通过index同步
|
||||||
/// <summary>
|
|
||||||
/// 直播使用。对齐各个轨道的起始。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="streams"></param>
|
|
||||||
/// <param name="takeLastCount"></param>
|
|
||||||
public static void SyncStreams(List<StreamSpec> selectedSteams, int takeLastCount = 15)
|
|
||||||
{
|
{
|
||||||
//通过Date同步
|
var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index));
|
||||||
if (selectedSteams.All(x => x.Playlist!.MediaParts[0].MediaSegments.All(x => x.DateTime != null)))
|
foreach (var item in selectedSteams)
|
||||||
{
|
{
|
||||||
var minDate = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.DateTime))!;
|
foreach (var part in item.Playlist!.MediaParts)
|
||||||
foreach (var item in selectedSteams)
|
|
||||||
{
|
{
|
||||||
foreach (var part in item.Playlist!.MediaParts)
|
part.MediaSegments = part.MediaSegments.Where(s => s.Index >= minIndex).ToList();
|
||||||
{
|
|
||||||
//秒级同步 忽略毫秒
|
|
||||||
part.MediaSegments = part.MediaSegments.Where(s => s.DateTime!.Value.Ticks / TimeSpan.TicksPerSecond >= minDate.Value.Ticks / TimeSpan.TicksPerSecond).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else //通过index同步
|
|
||||||
{
|
|
||||||
var minIndex = selectedSteams.Max(s => s.Playlist!.MediaParts[0].MediaSegments.Min(s => s.Index));
|
|
||||||
foreach (var item in selectedSteams)
|
|
||||||
{
|
|
||||||
foreach (var part in item.Playlist!.MediaParts)
|
|
||||||
{
|
|
||||||
part.MediaSegments = part.MediaSegments.Where(s => s.Index >= minIndex).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//取最新的N个分片
|
|
||||||
if (selectedSteams.Any(x => x.Playlist!.MediaParts[0].MediaSegments.Count > takeLastCount))
|
|
||||||
{
|
|
||||||
var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1;
|
|
||||||
if (skipCount < 0) skipCount = 0;
|
|
||||||
foreach (var item in selectedSteams)
|
|
||||||
{
|
|
||||||
foreach (var part in item.Playlist!.MediaParts)
|
|
||||||
{
|
|
||||||
part.MediaSegments = part.MediaSegments.Skip(skipCount).ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// 取最新的N个分片
|
||||||
/// 应用用户自定义的分片范围
|
if (selectedSteams.Any(x => x.Playlist!.MediaParts[0].MediaSegments.Count > takeLastCount))
|
||||||
/// </summary>
|
|
||||||
/// <param name="selectedSteams"></param>
|
|
||||||
/// <param name="customRange"></param>
|
|
||||||
public static void ApplyCustomRange(List<StreamSpec> selectedSteams, CustomRange? customRange)
|
|
||||||
{
|
{
|
||||||
var resultList = selectedSteams.Select(x => 0d).ToList();
|
var skipCount = selectedSteams.Min(x => x.Playlist!.MediaParts[0].MediaSegments.Count) - takeLastCount + 1;
|
||||||
|
if (skipCount < 0) skipCount = 0;
|
||||||
if (customRange == null) return;
|
foreach (var item in selectedSteams)
|
||||||
|
|
||||||
Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]");
|
|
||||||
Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]");
|
|
||||||
|
|
||||||
var filteByIndex = customRange.StartSegIndex != null && customRange.EndSegIndex != null;
|
|
||||||
var filteByTime = customRange.StartSec != null && customRange.EndSec != null;
|
|
||||||
|
|
||||||
if (!filteByIndex && !filteByTime)
|
|
||||||
{
|
{
|
||||||
Logger.ErrorMarkUp(ResString.customRangeInvalid);
|
foreach (var part in item.Playlist!.MediaParts)
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var stream in selectedSteams)
|
|
||||||
{
|
|
||||||
var skippedDur = 0d;
|
|
||||||
if (stream.Playlist == null) continue;
|
|
||||||
foreach (var part in stream.Playlist.MediaParts)
|
|
||||||
{
|
{
|
||||||
var newSegments = new List<MediaSegment>();
|
part.MediaSegments = part.MediaSegments.Skip(skipCount).ToList();
|
||||||
if (filteByIndex)
|
|
||||||
newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList();
|
|
||||||
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
|
|
||||||
&& stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) <= customRange.EndSec).ToList();
|
|
||||||
|
|
||||||
if (newSegments.Count > 0)
|
|
||||||
skippedDur += part.MediaSegments.Where(seg => seg.Index < newSegments.First().Index).Sum(x => x.Duration);
|
|
||||||
part.MediaSegments = newSegments;
|
|
||||||
}
|
|
||||||
stream.SkippedDuration = skippedDur;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 根据用户输入,清除广告分片
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="selectedSteams"></param>
|
|
||||||
/// <param name="customRange"></param>
|
|
||||||
public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords)
|
|
||||||
{
|
|
||||||
if (keywords == null) return;
|
|
||||||
var regList = keywords.Select(s => new Regex(s));
|
|
||||||
foreach ( var reg in regList)
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var stream in selectedSteams)
|
|
||||||
{
|
|
||||||
if (stream.Playlist == null) continue;
|
|
||||||
|
|
||||||
var countBefore = stream.SegmentsCount;
|
|
||||||
|
|
||||||
foreach (var part in stream.Playlist.MediaParts)
|
|
||||||
{
|
|
||||||
//没有找到广告分片
|
|
||||||
if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url))))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
//找到广告分片 清理
|
|
||||||
else
|
|
||||||
{
|
|
||||||
part.MediaSegments = part.MediaSegments.Where(x => regList.All(reg => !reg.IsMatch(x.Url))).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//清理已经为空的 part
|
|
||||||
stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList();
|
|
||||||
|
|
||||||
var countAfter = stream.SegmentsCount;
|
|
||||||
|
|
||||||
if (countBefore != countAfter)
|
|
||||||
{
|
|
||||||
Logger.WarnMarkUp("[grey]{} segments => {} segments[/]", countBefore, countAfter);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用用户自定义的分片范围
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="selectedSteams"></param>
|
||||||
|
/// <param name="customRange"></param>
|
||||||
|
public static void ApplyCustomRange(List<StreamSpec> selectedSteams, CustomRange? customRange)
|
||||||
|
{
|
||||||
|
if (customRange == null) return;
|
||||||
|
|
||||||
|
Logger.InfoMarkUp($"{ResString.customRangeFound}[Cyan underline]{customRange.InputStr}[/]");
|
||||||
|
Logger.WarnMarkUp($"[darkorange3_1]{ResString.customRangeWarn}[/]");
|
||||||
|
|
||||||
|
var filterByIndex = customRange is { StartSegIndex: not null, EndSegIndex: not null };
|
||||||
|
var filterByTime = customRange is { StartSec: not null, EndSec: not null };
|
||||||
|
|
||||||
|
if (!filterByIndex && !filterByTime)
|
||||||
|
{
|
||||||
|
Logger.ErrorMarkUp(ResString.customRangeInvalid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var stream in selectedSteams)
|
||||||
|
{
|
||||||
|
var skippedDur = 0d;
|
||||||
|
if (stream.Playlist == null) continue;
|
||||||
|
foreach (var part in stream.Playlist.MediaParts)
|
||||||
|
{
|
||||||
|
List<MediaSegment> newSegments;
|
||||||
|
if (filterByIndex)
|
||||||
|
newSegments = part.MediaSegments.Where(seg => seg.Index >= customRange.StartSegIndex && seg.Index <= customRange.EndSegIndex).ToList();
|
||||||
|
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
|
||||||
|
&& stream.Playlist.MediaParts.SelectMany(p => p.MediaSegments).Where(x => x.Index < seg.Index).Sum(x => x.Duration) <= customRange.EndSec).ToList();
|
||||||
|
|
||||||
|
if (newSegments.Count > 0)
|
||||||
|
skippedDur += part.MediaSegments.Where(seg => seg.Index < newSegments.First().Index).Sum(x => x.Duration);
|
||||||
|
part.MediaSegments = newSegments;
|
||||||
|
}
|
||||||
|
stream.SkippedDuration = skippedDur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据用户输入,清除广告分片
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="selectedSteams"></param>
|
||||||
|
/// <param name="keywords"></param>
|
||||||
|
public static void CleanAd(List<StreamSpec> selectedSteams, string[]? keywords)
|
||||||
|
{
|
||||||
|
if (keywords == null) return;
|
||||||
|
var regList = keywords.Select(s => new Regex(s)).ToList();
|
||||||
|
foreach ( var reg in regList)
|
||||||
|
{
|
||||||
|
Logger.InfoMarkUp($"{ResString.customAdKeywordsFound}[Cyan underline]{reg}[/]");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var stream in selectedSteams)
|
||||||
|
{
|
||||||
|
if (stream.Playlist == null) continue;
|
||||||
|
|
||||||
|
var countBefore = stream.SegmentsCount;
|
||||||
|
|
||||||
|
foreach (var part in stream.Playlist.MediaParts)
|
||||||
|
{
|
||||||
|
// 没有找到广告分片
|
||||||
|
if (part.MediaSegments.All(x => regList.All(reg => !reg.IsMatch(x.Url))))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 找到广告分片 清理
|
||||||
|
part.MediaSegments = part.MediaSegments.Where(x => regList.All(reg => !reg.IsMatch(x.Url))).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理已经为空的 part
|
||||||
|
stream.Playlist.MediaParts = stream.Playlist.MediaParts.Where(x => x.MediaSegments.Count > 0).ToList();
|
||||||
|
|
||||||
|
var countAfter = stream.SegmentsCount;
|
||||||
|
|
||||||
|
if (countBefore != countAfter)
|
||||||
|
{
|
||||||
|
Logger.WarnMarkUp("[grey]{} segments => {} segments[/]", countBefore, countAfter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,76 +1,43 @@
|
|||||||
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;
|
||||||
{
|
// PNG HEADER检测
|
||||||
var size = bArr.Length;
|
if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3])
|
||||||
//PNG HEADER检测
|
return true;
|
||||||
if (size > 3 && 137 == bArr[0] && 80 == bArr[1] && 78 == bArr[2] && 71 == bArr[3])
|
// GIF HEADER检测
|
||||||
return true;
|
if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3])
|
||||||
//GIF HEADER检测
|
return true;
|
||||||
else if (size > 3 && 0x47 == bArr[0] && 0x49 == bArr[1] && 0x46 == bArr[2] && 0x38 == bArr[3])
|
// BMP HEADER检测
|
||||||
return true;
|
if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8])
|
||||||
//BMP HEADER检测
|
return true;
|
||||||
else if (size > 10 && 0x42 == bArr[0] && 0x4D == bArr[1] && 0x00 == bArr[5] && 0x00 == bArr[6] && 0x00 == bArr[7] && 0x00 == bArr[8])
|
// JPEG HEADER检测
|
||||||
return true;
|
if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2])
|
||||||
//JPEG HEADER检测
|
return true;
|
||||||
else if (size > 3 && 0xFF == bArr[0] && 0xD8 == bArr[1] && 0xFF == bArr[2])
|
return false;
|
||||||
return true;
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task ProcessAsync(string sourcePath)
|
public static async Task ProcessAsync(string sourcePath)
|
||||||
{
|
{
|
||||||
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])
|
||||||
|
sourceData = sourceData[120..];
|
||||||
|
else if (sourceData.Length > 6102 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[6100] && 130 == sourceData[6101])
|
||||||
|
sourceData = sourceData[6102..];
|
||||||
|
else if (sourceData.Length > 69 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[67] && 130 == sourceData[68])
|
||||||
|
sourceData = sourceData[69..];
|
||||||
|
else if (sourceData.Length > 771 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[769] && 130 == sourceData[770])
|
||||||
|
sourceData = sourceData[771..];
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (sourceData.Length > 120 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[118] && 130 == sourceData[119])
|
// 手动查询结尾标记 0x47 出现两次
|
||||||
sourceData = sourceData[120..];
|
|
||||||
else if (sourceData.Length > 6102 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[6100] && 130 == sourceData[6101])
|
|
||||||
sourceData = sourceData[6102..];
|
|
||||||
else if (sourceData.Length > 69 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[67] && 130 == sourceData[68])
|
|
||||||
sourceData = sourceData[69..];
|
|
||||||
else if (sourceData.Length > 771 && 137 == sourceData[0] && 80 == sourceData[1] && 78 == sourceData[2] && 71 == sourceData[3] && 96 == sourceData[769] && 130 == sourceData[770])
|
|
||||||
sourceData = sourceData[771..];
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//手动查询结尾标记 0x47 出现两次
|
|
||||||
int skip = 0;
|
|
||||||
for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)
|
|
||||||
{
|
|
||||||
if (sourceData[i] == 0x47 && sourceData[i + 188] == 0x47 && sourceData[i + 188 + 188] == 0x47)
|
|
||||||
{
|
|
||||||
skip = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceData = sourceData[skip..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//GIF HEADER
|
|
||||||
else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3])
|
|
||||||
{
|
|
||||||
sourceData = sourceData[42..];
|
|
||||||
}
|
|
||||||
//BMP HEADER
|
|
||||||
else if (0x42 == sourceData[0] && 0x4D == sourceData[1] && 0x00 == sourceData[5] && 0x00 == sourceData[6] && 0x00 == sourceData[7] && 0x00 == sourceData[8])
|
|
||||||
{
|
|
||||||
sourceData = sourceData[0x3E..];
|
|
||||||
}
|
|
||||||
//JPEG HEADER检测
|
|
||||||
else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2])
|
|
||||||
{
|
|
||||||
//手动查询结尾标记 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++)
|
||||||
{
|
{
|
||||||
@ -82,8 +49,33 @@ namespace N_m3u8DL_RE.Util
|
|||||||
}
|
}
|
||||||
sourceData = sourceData[skip..];
|
sourceData = sourceData[skip..];
|
||||||
}
|
}
|
||||||
|
|
||||||
await File.WriteAllBytesAsync(sourcePath, sourceData);
|
|
||||||
}
|
}
|
||||||
|
// GIF HEADER
|
||||||
|
else if (0x47 == sourceData[0] && 0x49 == sourceData[1] && 0x46 == sourceData[2] && 0x38 == sourceData[3])
|
||||||
|
{
|
||||||
|
sourceData = sourceData[42..];
|
||||||
|
}
|
||||||
|
// BMP HEADER
|
||||||
|
else if (0x42 == sourceData[0] && 0x4D == sourceData[1] && 0x00 == sourceData[5] && 0x00 == sourceData[6] && 0x00 == sourceData[7] && 0x00 == sourceData[8])
|
||||||
|
{
|
||||||
|
sourceData = sourceData[0x3E..];
|
||||||
|
}
|
||||||
|
// JPEG HEADER检测
|
||||||
|
else if (0xFF == sourceData[0] && 0xD8 == sourceData[1] && 0xFF == sourceData[2])
|
||||||
|
{
|
||||||
|
// 手动查询结尾标记 0x47 出现两次
|
||||||
|
int skip = 0;
|
||||||
|
for (int i = 4; i < sourceData.Length - 188 * 2 - 4; i++)
|
||||||
|
{
|
||||||
|
if (sourceData[i] == 0x47 && sourceData[i + 188] == 0x47 && sourceData[i + 188 + 188] == 0x47)
|
||||||
|
{
|
||||||
|
skip = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceData = sourceData[skip..];
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(sourcePath, sourceData);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,13 +374,13 @@ 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
|
||||||
ar;ara
|
ar;ara
|
||||||
bg;bul
|
bg;bul
|
||||||
@ -500,48 +486,46 @@ 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>
|
|
||||||
/// 转换 ISO 639-1 => ISO 639-2
|
|
||||||
/// 且当Description为空时将DisplayName写入
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputFile"></param>
|
|
||||||
public static void ConvertLangCodeAndDisplayName(OutputFile outputFile)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(outputFile.LangCode)) return;
|
|
||||||
var originalLangCode = outputFile.LangCode;
|
|
||||||
|
|
||||||
//先直接查找
|
|
||||||
var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase));
|
|
||||||
//处理特殊的扩展语言标记
|
|
||||||
if (lang == null)
|
|
||||||
{
|
|
||||||
//2位转3位
|
|
||||||
var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First());
|
|
||||||
lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lang != null)
|
|
||||||
{
|
|
||||||
outputFile.LangCode = lang.Code;
|
|
||||||
if (string.IsNullOrEmpty(outputFile.Description))
|
|
||||||
outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio;
|
|
||||||
}
|
|
||||||
else if (outputFile.LangCode == null)
|
|
||||||
{
|
|
||||||
outputFile.LangCode = "und"; //无法识别直接置为und
|
|
||||||
}
|
|
||||||
|
|
||||||
//无描述,则把LangCode当作描述
|
|
||||||
if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换 ISO 639-1 => ISO 639-2
|
||||||
|
/// 且当Description为空时将DisplayName写入
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outputFile"></param>
|
||||||
|
public static void ConvertLangCodeAndDisplayName(OutputFile outputFile)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(outputFile.LangCode)) return;
|
||||||
|
var originalLangCode = outputFile.LangCode;
|
||||||
|
|
||||||
|
// 先直接查找
|
||||||
|
var lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(outputFile.LangCode, StringComparison.OrdinalIgnoreCase));
|
||||||
|
// 处理特殊的扩展语言标记
|
||||||
|
if (lang == null)
|
||||||
|
{
|
||||||
|
// 2位转3位
|
||||||
|
var l = ConvertTwoToThree(outputFile.LangCode.Split('-').First());
|
||||||
|
lang = ALL_LANGS.FirstOrDefault(a => a.ExtendCode.Equals(l, StringComparison.OrdinalIgnoreCase) || a.Code.Equals(l, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lang != null)
|
||||||
|
{
|
||||||
|
outputFile.LangCode = lang.Code;
|
||||||
|
if (string.IsNullOrEmpty(outputFile.Description))
|
||||||
|
outputFile.Description = outputFile.MediaType == Common.Enum.MediaType.SUBTITLES ? lang.Description : lang.DescriptionAudio;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputFile.LangCode = "und"; // 无法识别直接置为und
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无描述,则把LangCode当作描述
|
||||||
|
if (string.IsNullOrEmpty(outputFile.Description)) outputFile.Description = originalLangCode;
|
||||||
|
}
|
||||||
|
}
|
@ -1,122 +1,113 @@
|
|||||||
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 long From;
|
||||||
|
public required long To;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL大文件切片处理
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="segment"></param>
|
||||||
|
/// <param name="headers"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers)
|
||||||
|
{
|
||||||
|
var url = segment.Url;
|
||||||
|
if (!await CanSplitAsync(url, headers)) return null;
|
||||||
|
|
||||||
|
if (segment.StartRange != null) return null;
|
||||||
|
|
||||||
|
long fileSize = await GetFileSizeAsync(url, headers);
|
||||||
|
if (fileSize == 0) return null;
|
||||||
|
|
||||||
|
List<Clip> allClips = GetAllClips(url, fileSize);
|
||||||
|
var splitSegments = new List<MediaSegment>();
|
||||||
|
foreach (Clip clip in allClips)
|
||||||
{
|
{
|
||||||
public required int index;
|
splitSegments.Add(new MediaSegment()
|
||||||
public required long from;
|
{
|
||||||
public required long to;
|
Index = clip.Index,
|
||||||
|
Url = url,
|
||||||
|
StartRange = clip.From,
|
||||||
|
ExpectLength = clip.To == -1 ? null : clip.To - clip.From + 1,
|
||||||
|
EncryptInfo = segment.EncryptInfo,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return splitSegments;
|
||||||
/// URL大文件切片处理
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
public static async Task<bool> CanSplitAsync(string url, Dictionary<string, string> headers)
|
||||||
/// <param name="headers"></param>
|
{
|
||||||
/// <param name="splitSegments"></param>
|
try
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<List<MediaSegment>?> SplitUrlAsync(MediaSegment segment, Dictionary<string,string> headers)
|
|
||||||
{
|
{
|
||||||
var url = segment.Url;
|
var request = new HttpRequestMessage(HttpMethod.Head, url);
|
||||||
if (!await CanSplitAsync(url, headers)) return null;
|
var response = (await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
|
||||||
|
bool supportsRangeRequests = response.Headers.Contains("Accept-Ranges");
|
||||||
|
|
||||||
if (segment.StartRange != null) return null;
|
return supportsRangeRequests;
|
||||||
|
|
||||||
long fileSize = await GetFileSizeAsync(url, headers);
|
|
||||||
if (fileSize == 0) return null;
|
|
||||||
|
|
||||||
List<Clip> allClips = GetAllClips(url, fileSize);
|
|
||||||
var splitSegments = new List<MediaSegment>();
|
|
||||||
foreach (Clip clip in allClips)
|
|
||||||
{
|
|
||||||
splitSegments.Add(new MediaSegment()
|
|
||||||
{
|
|
||||||
Index = clip.index,
|
|
||||||
Url = url,
|
|
||||||
StartRange = clip.from,
|
|
||||||
ExpectLength = clip.to == -1 ? null : clip.to - clip.from + 1,
|
|
||||||
EncryptInfo = segment.EncryptInfo,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return splitSegments;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
public static async Task<bool> CanSplitAsync(string url, Dictionary<string, string> headers)
|
|
||||||
{
|
{
|
||||||
try
|
Logger.DebugMarkUp(ex.Message);
|
||||||
{
|
return false;
|
||||||
var request = new HttpRequestMessage(HttpMethod.Head, url);
|
|
||||||
var response = (await HTTPUtil.AppHttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
|
|
||||||
bool supportsRangeRequests = response.Headers.Contains("Accept-Ranges");
|
|
||||||
|
|
||||||
return supportsRangeRequests;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.DebugMarkUp(ex.Message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<long> GetFileSizeAsync(string url, Dictionary<string, string> headers)
|
|
||||||
{
|
|
||||||
using var httpRequestMessage = new HttpRequestMessage();
|
|
||||||
httpRequestMessage.RequestUri = new(url);
|
|
||||||
foreach (var header in headers)
|
|
||||||
{
|
|
||||||
httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
var response = (await HTTPUtil.AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
|
|
||||||
long totalSizeBytes = response.Content.Headers.ContentLength ?? 0;
|
|
||||||
|
|
||||||
return totalSizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
//此函数主要是切片下载逻辑
|
|
||||||
private static List<Clip> GetAllClips(string url, long fileSize)
|
|
||||||
{
|
|
||||||
List<Clip> clips = new();
|
|
||||||
int index = 0;
|
|
||||||
long counter = 0;
|
|
||||||
int perSize = 10 * 1024 * 1024;
|
|
||||||
while (fileSize > 0)
|
|
||||||
{
|
|
||||||
Clip c = new()
|
|
||||||
{
|
|
||||||
index = index,
|
|
||||||
from = counter,
|
|
||||||
to = counter + perSize
|
|
||||||
};
|
|
||||||
//没到最后
|
|
||||||
if (fileSize - perSize > 0)
|
|
||||||
{
|
|
||||||
fileSize -= perSize;
|
|
||||||
counter += perSize + 1;
|
|
||||||
index++;
|
|
||||||
clips.Add(c);
|
|
||||||
}
|
|
||||||
//已到最后
|
|
||||||
else
|
|
||||||
{
|
|
||||||
c.to = -1;
|
|
||||||
clips.Add(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clips;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static async Task<long> GetFileSizeAsync(string url, Dictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
using var httpRequestMessage = new HttpRequestMessage();
|
||||||
|
httpRequestMessage.RequestUri = new(url);
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
var response = (await HTTPUtil.AppHttpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead)).EnsureSuccessStatusCode();
|
||||||
|
long totalSizeBytes = response.Content.Headers.ContentLength ?? 0;
|
||||||
|
|
||||||
|
return totalSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 此函数主要是切片下载逻辑
|
||||||
|
private static List<Clip> GetAllClips(string url, long fileSize)
|
||||||
|
{
|
||||||
|
List<Clip> clips = [];
|
||||||
|
int index = 0;
|
||||||
|
long counter = 0;
|
||||||
|
int perSize = 10 * 1024 * 1024;
|
||||||
|
while (fileSize > 0)
|
||||||
|
{
|
||||||
|
Clip c = new()
|
||||||
|
{
|
||||||
|
Index = index,
|
||||||
|
From = counter,
|
||||||
|
To = counter + perSize
|
||||||
|
};
|
||||||
|
// 没到最后
|
||||||
|
if (fileSize - perSize > 0)
|
||||||
|
{
|
||||||
|
fileSize -= perSize;
|
||||||
|
counter += perSize + 1;
|
||||||
|
index++;
|
||||||
|
clips.Add(c);
|
||||||
|
}
|
||||||
|
// 已到最后
|
||||||
|
else
|
||||||
|
{
|
||||||
|
c.To = -1;
|
||||||
|
clips.Add(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clips;
|
||||||
|
}
|
||||||
|
}
|
@ -1,180 +1,219 @@
|
|||||||
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";
|
if (keys == null || keys.Length == 0) return false;
|
||||||
public static async Task<bool> DecryptAsync(bool shakaPackager, string bin, string[]? keys, string source, string dest, string? kid, string init = "", bool isMultiDRM=false)
|
|
||||||
|
var keyPairs = keys.ToList();
|
||||||
|
string? keyPair = null;
|
||||||
|
string? trackId = null;
|
||||||
|
string? tmpEncFile = null;
|
||||||
|
string? tmpDecFile = null;
|
||||||
|
string? workDir = null;
|
||||||
|
|
||||||
|
if (isMultiDRM)
|
||||||
{
|
{
|
||||||
if (keys == null || keys.Length == 0) return false;
|
trackId = "1";
|
||||||
|
}
|
||||||
|
|
||||||
string? keyPair = null;
|
if (!string.IsNullOrEmpty(kid))
|
||||||
string? trackId = null;
|
{
|
||||||
|
var test = keyPairs.Where(k => k.StartsWith(kid)).ToList();
|
||||||
|
if (test.Count != 0) keyPair = test.First();
|
||||||
|
}
|
||||||
|
|
||||||
if (isMultiDRM)
|
// Apple
|
||||||
|
if (kid == ZeroKid)
|
||||||
|
{
|
||||||
|
keyPair = keyPairs.First();
|
||||||
|
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;
|
||||||
|
|
||||||
|
// shakaPackager/ffmpeg 无法单独解密init文件
|
||||||
|
if (source.EndsWith("_init.mp4") && decryptEngine != DecryptEngine.MP4DECRYPT) return false;
|
||||||
|
|
||||||
|
string cmd;
|
||||||
|
|
||||||
|
var tmpFile = "";
|
||||||
|
if (decryptEngine == DecryptEngine.SHAKA_PACKAGER)
|
||||||
|
{
|
||||||
|
var enc = source;
|
||||||
|
// shakaPackager 手动构造文件
|
||||||
|
if (init != "")
|
||||||
{
|
{
|
||||||
trackId = "1";
|
tmpFile = Path.ChangeExtension(source, ".itmp");
|
||||||
|
MergeUtil.CombineMultipleFilesIntoSingleFile([init, source], tmpFile);
|
||||||
|
enc = tmpFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(kid))
|
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]}";
|
||||||
|
}
|
||||||
|
else if (decryptEngine == DecryptEngine.MP4DECRYPT)
|
||||||
|
{
|
||||||
|
if (trackId == null)
|
||||||
{
|
{
|
||||||
var test = keys.Where(k => k.StartsWith(kid));
|
cmd = string.Join(" ", keyPairs.Select(k => $"--key {k}"));
|
||||||
if (test.Any()) keyPair = test.First();
|
|
||||||
}
|
|
||||||
|
|
||||||
//Apple
|
|
||||||
if (kid == ZeroKid)
|
|
||||||
{
|
|
||||||
keyPair = keys.First();
|
|
||||||
trackId = "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keyPair == null) return false;
|
|
||||||
|
|
||||||
//shakaPackager 无法单独解密init文件
|
|
||||||
if (source.EndsWith("_init.mp4") && shakaPackager) return false;
|
|
||||||
|
|
||||||
var cmd = "";
|
|
||||||
|
|
||||||
var tmpFile = "";
|
|
||||||
if (shakaPackager)
|
|
||||||
{
|
|
||||||
var enc = source;
|
|
||||||
//shakaPackager 手动构造文件
|
|
||||||
if (init != "")
|
|
||||||
{
|
|
||||||
tmpFile = Path.ChangeExtension(source, ".itmp");
|
|
||||||
MergeUtil.CombineMultipleFilesIntoSingleFile(new string[] { init, source }, tmpFile);
|
|
||||||
enc = tmpFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
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]}";
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (trackId == null)
|
cmd = string.Join(" ", keyPairs.Select(k => $"--key {trackId}:{k.Split(':')[1]}"));
|
||||||
{
|
|
||||||
cmd = string.Join(" ", keys.Select(k => $"--key {k}"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cmd = string.Join(" ", keys.Select(k => $"--key {trackId}:{k.Split(':')[1]}"));
|
|
||||||
}
|
|
||||||
if (init != "")
|
|
||||||
{
|
|
||||||
cmd += $" --fragments-info \"{init}\" ";
|
|
||||||
}
|
|
||||||
cmd += $" \"{source}\" \"{dest}\"";
|
|
||||||
}
|
}
|
||||||
|
// 解决mp4decrypt中文问题 切换到源文件所在目录并改名再解密
|
||||||
await RunCommandAsync(bin, cmd);
|
workDir = Path.GetDirectoryName(source)!;
|
||||||
|
tmpEncFile = Path.Combine(workDir, $"{Guid.NewGuid()}{Path.GetExtension(source)}");
|
||||||
if (File.Exists(dest) && new FileInfo(dest).Length > 0)
|
tmpDecFile = Path.Combine(workDir, $"{Path.GetFileNameWithoutExtension(tmpEncFile)}_dec{Path.GetExtension(tmpEncFile)}");
|
||||||
|
File.Move(source, tmpEncFile);
|
||||||
|
if (init != "")
|
||||||
{
|
{
|
||||||
if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile);
|
var infoFile = Path.GetDirectoryName(init) == workDir ? Path.GetFileName(init) : init;
|
||||||
return true;
|
cmd += $" --fragments-info \"{infoFile}\" ";
|
||||||
}
|
}
|
||||||
|
cmd += $" \"{Path.GetFileName(tmpEncFile)}\" \"{Path.GetFileName(tmpDecFile)}\"";
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private static async Task RunCommandAsync(string name, string arg)
|
|
||||||
{
|
{
|
||||||
Logger.DebugMarkUp($"FileName: {name}");
|
var enc = source;
|
||||||
Logger.DebugMarkUp($"Arguments: {arg}");
|
// ffmpeg实时解密 手动构造文件
|
||||||
await Process.Start(new ProcessStartInfo()
|
if (init != "")
|
||||||
{
|
{
|
||||||
FileName = name,
|
tmpFile = Path.ChangeExtension(source, ".itmp");
|
||||||
Arguments = arg,
|
MergeUtil.CombineMultipleFilesIntoSingleFile([init, source], tmpFile);
|
||||||
//RedirectStandardOutput = true,
|
enc = tmpFile;
|
||||||
//RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
})!.WaitForExitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从文本文件中查询KID的KEY
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="file">文本文件</param>
|
|
||||||
/// <param name="kid">目标KID</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<string?> SearchKeyFromFileAsync(string? file, string? kid)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(file) || !File.Exists(file) || string.IsNullOrEmpty(kid))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
Logger.InfoMarkUp(ResString.searchKey);
|
|
||||||
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
using var reader = new StreamReader(stream);
|
|
||||||
var line = "";
|
|
||||||
while ((line = await reader.ReadLineAsync()) != null)
|
|
||||||
{
|
|
||||||
if (line.Trim().StartsWith(kid))
|
|
||||||
{
|
|
||||||
Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]");
|
|
||||||
return line.Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
cmd = $"-loglevel error -nostdin -decryption_key {keyPair.Split(':')[1]} -i \"{enc}\" -c copy \"{dest}\"";
|
||||||
Logger.ErrorMarkUp(ex.Message);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ParsedMP4Info GetMP4Info(byte[] data)
|
var isSuccess = await RunCommandAsync(bin, cmd, workDir);
|
||||||
|
|
||||||
|
// mp4decrypt 还原文件改名操作
|
||||||
|
if (workDir is not null)
|
||||||
{
|
{
|
||||||
var info = MP4InitUtil.ReadInit(data);
|
if (File.Exists(tmpEncFile)) File.Move(tmpEncFile, source);
|
||||||
if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]");
|
if (File.Exists(tmpDecFile)) File.Move(tmpDecFile, dest);
|
||||||
if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]");
|
|
||||||
if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]");
|
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ParsedMP4Info GetMP4Info(string output)
|
if (isSuccess)
|
||||||
{
|
{
|
||||||
using (var fs = File.OpenRead(output))
|
if (tmpFile != "" && File.Exists(tmpFile)) File.Delete(tmpFile);
|
||||||
{
|
return true;
|
||||||
var header = new byte[1 * 1024 * 1024]; //1MB
|
|
||||||
fs.Read(header);
|
|
||||||
return GetMP4Info(header);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string? ReadInitShaka(string output, string bin)
|
|
||||||
{
|
|
||||||
Regex ShakaKeyIDRegex = new Regex("Key for key_id=([0-9a-f]+) was not found");
|
|
||||||
|
|
||||||
// TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid)
|
|
||||||
// - stop process
|
|
||||||
// - remove {output}.tmp.webm
|
|
||||||
var cmd = $"--quiet --enable_raw_key_decryption input=\"{output}\",stream=0,output=\"{output}.tmp.webm\" " +
|
|
||||||
$"--keys key_id={ZeroKid}:key={ZeroKid}";
|
|
||||||
|
|
||||||
using var p = new Process();
|
|
||||||
p.StartInfo = new ProcessStartInfo()
|
|
||||||
{
|
|
||||||
FileName = bin,
|
|
||||||
Arguments = cmd,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
};
|
|
||||||
p.Start();
|
|
||||||
var errorOutput = p.StandardError.ReadToEnd();
|
|
||||||
p.WaitForExit();
|
|
||||||
return ShakaKeyIDRegex.Match(errorOutput).Groups[1].Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.Error(ResString.decryptionFailed);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static async Task<bool> RunCommandAsync(string name, string arg, string? workDir = null)
|
||||||
|
{
|
||||||
|
Logger.DebugMarkUp($"FileName: {name}");
|
||||||
|
Logger.DebugMarkUp($"Arguments: {arg}");
|
||||||
|
var process = Process.Start(new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
FileName = name,
|
||||||
|
Arguments = arg,
|
||||||
|
// RedirectStandardOutput = true,
|
||||||
|
// RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = workDir
|
||||||
|
});
|
||||||
|
await process!.WaitForExitAsync();
|
||||||
|
return process.ExitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从文本文件中查询KID的KEY
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="file">文本文件</param>
|
||||||
|
/// <param name="kid">目标KID</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<string?> SearchKeyFromFileAsync(string? file, string? kid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(file) || !File.Exists(file) || string.IsNullOrEmpty(kid))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Logger.InfoMarkUp(ResString.searchKey);
|
||||||
|
using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
while (await reader.ReadLineAsync() is { } line)
|
||||||
|
{
|
||||||
|
if (!line.Trim().StartsWith(kid)) continue;
|
||||||
|
|
||||||
|
Logger.InfoMarkUp($"[green]OK[/] [grey]{line.Trim()}[/]");
|
||||||
|
return line.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.ErrorMarkUp(ex.Message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ParsedMP4Info GetMP4Info(byte[] data)
|
||||||
|
{
|
||||||
|
var info = MP4InitUtil.ReadInit(data);
|
||||||
|
if (info.Scheme != null) Logger.WarnMarkUp($"[grey]Type: {info.Scheme}[/]");
|
||||||
|
if (info.PSSH != null) Logger.WarnMarkUp($"[grey]PSSH(WV): {info.PSSH}[/]");
|
||||||
|
if (info.KID != null) Logger.WarnMarkUp($"[grey]KID: {info.KID}[/]");
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ParsedMP4Info GetMP4Info(string output)
|
||||||
|
{
|
||||||
|
using var fs = File.OpenRead(output);
|
||||||
|
var header = new byte[1 * 1024 * 1024]; // 1MB
|
||||||
|
_ = fs.Read(header);
|
||||||
|
return GetMP4Info(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ReadInitShaka(string output, string bin)
|
||||||
|
{
|
||||||
|
Regex shakaKeyIdRegex = KidOutputRegex();
|
||||||
|
|
||||||
|
// TODO: handle the case that shaka packager actually decrypted (key ID == ZeroKid)
|
||||||
|
// - stop process
|
||||||
|
// - remove {output}.tmp.webm
|
||||||
|
var cmd = $"--quiet --enable_raw_key_decryption input=\"{output}\",stream=0,output=\"{output}.tmp.webm\" " +
|
||||||
|
$"--keys key_id={ZeroKid}:key={ZeroKid}";
|
||||||
|
|
||||||
|
using var p = new Process();
|
||||||
|
p.StartInfo = new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
FileName = bin,
|
||||||
|
Arguments = cmd,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
p.Start();
|
||||||
|
var errorOutput = p.StandardError.ReadToEnd();
|
||||||
|
p.WaitForExit();
|
||||||
|
return shakaKeyIdRegex.Match(errorOutput).Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("Key for key_id=([0-9a-f]+) was not found")]
|
||||||
|
private static partial Regex KidOutputRegex();
|
||||||
|
}
|
@ -1,99 +1,92 @@
|
|||||||
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 #.*")]
|
||||||
|
private static partial Regex TextRegex();
|
||||||
|
[GeneratedRegex(@"#0:\d(\[0x\w+?\])")]
|
||||||
|
private static partial Regex IdRegex();
|
||||||
|
[GeneratedRegex(": (\\w+): (.*)")]
|
||||||
|
private static partial Regex TypeRegex();
|
||||||
|
[GeneratedRegex("(.*?)(,|$)")]
|
||||||
|
private static partial Regex BaseInfoRegex();
|
||||||
|
[GeneratedRegex(@" \/ 0x\w+")]
|
||||||
|
private static partial Regex ReplaceRegex();
|
||||||
|
[GeneratedRegex(@"\d{2,}x\d+")]
|
||||||
|
private static partial Regex ResRegex();
|
||||||
|
[GeneratedRegex(@"\d+ kb\/s")]
|
||||||
|
private static partial Regex BitrateRegex();
|
||||||
|
[GeneratedRegex(@"(\d+(\.\d+)?) fps")]
|
||||||
|
private static partial Regex FpsRegex();
|
||||||
|
[GeneratedRegex(@"DOVI configuration record.*profile: (\d).*compatibility id: (\d)")]
|
||||||
|
private static partial Regex DoViRegex();
|
||||||
|
[GeneratedRegex(@"Duration.*?start: (\d+\.?\d{0,3})")]
|
||||||
|
private static partial Regex StartRegex();
|
||||||
|
|
||||||
|
public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file)
|
||||||
{
|
{
|
||||||
[GeneratedRegex(" Stream #.*")]
|
var result = new List<Mediainfo>();
|
||||||
private static partial Regex TextRegex();
|
|
||||||
[GeneratedRegex("#0:\\d(\\[0x\\w+?\\])")]
|
|
||||||
private static partial Regex IdRegex();
|
|
||||||
[GeneratedRegex(": (\\w+): (.*)")]
|
|
||||||
private static partial Regex TypeRegex();
|
|
||||||
[GeneratedRegex("(.*?)(,|$)")]
|
|
||||||
private static partial Regex BaseInfoRegex();
|
|
||||||
[GeneratedRegex(" \\/ 0x\\w+")]
|
|
||||||
private static partial Regex ReplaceRegex();
|
|
||||||
[GeneratedRegex("\\d{2,}x\\d+")]
|
|
||||||
private static partial Regex ResRegex();
|
|
||||||
[GeneratedRegex("\\d+ kb\\/s")]
|
|
||||||
private static partial Regex BitrateRegex();
|
|
||||||
[GeneratedRegex("(\\d+(\\.\\d+)?) fps")]
|
|
||||||
private static partial Regex FpsRegex();
|
|
||||||
[GeneratedRegex("DOVI configuration record.*profile: (\\d).*compatibility id: (\\d)")]
|
|
||||||
private static partial Regex DoViRegex();
|
|
||||||
[GeneratedRegex("Duration.*?start: (\\d+\\.?\\d{0,3})")]
|
|
||||||
private static partial Regex StartRegex();
|
|
||||||
|
|
||||||
public static async Task<List<Mediainfo>> ReadInfoAsync(string binary, string file)
|
if (string.IsNullOrEmpty(file) || !File.Exists(file)) return result;
|
||||||
|
|
||||||
|
string cmd = "-hide_banner -i \"" + file + "\"";
|
||||||
|
var p = Process.Start(new ProcessStartInfo()
|
||||||
{
|
{
|
||||||
var result = new List<Mediainfo>();
|
FileName = binary,
|
||||||
|
Arguments = cmd,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
})!;
|
||||||
|
var output = await p.StandardError.ReadToEndAsync();
|
||||||
|
await p.WaitForExitAsync();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(file) || !File.Exists(file)) return result;
|
foreach (Match stream in TextRegex().Matches(output))
|
||||||
|
{
|
||||||
string cmd = "-hide_banner -i \"" + file + "\"";
|
var info = new Mediainfo()
|
||||||
var p = Process.Start(new ProcessStartInfo()
|
|
||||||
{
|
{
|
||||||
FileName = binary,
|
Text = TypeRegex().Match(stream.Value).Groups[2].Value.TrimEnd(),
|
||||||
Arguments = cmd,
|
Id = IdRegex().Match(stream.Value).Groups[1].Value,
|
||||||
RedirectStandardOutput = true,
|
Type = TypeRegex().Match(stream.Value).Groups[1].Value,
|
||||||
RedirectStandardError = true,
|
};
|
||||||
UseShellExecute = false
|
|
||||||
})!;
|
|
||||||
var output = p.StandardError.ReadToEnd();
|
|
||||||
await p.WaitForExitAsync();
|
|
||||||
|
|
||||||
foreach (Match stream in TextRegex().Matches(output))
|
info.Resolution = ResRegex().Match(info.Text).Value;
|
||||||
|
info.Bitrate = BitrateRegex().Match(info.Text).Value;
|
||||||
|
info.Fps = FpsRegex().Match(info.Text).Value;
|
||||||
|
info.BaseInfo = BaseInfoRegex().Match(info.Text).Groups[1].Value;
|
||||||
|
info.BaseInfo = ReplaceRegex().Replace(info.BaseInfo, "");
|
||||||
|
info.HDR = info.Text.Contains("/bt2020/");
|
||||||
|
|
||||||
|
if (info.BaseInfo.Contains("dvhe")
|
||||||
|
|| info.BaseInfo.Contains("dvh1")
|
||||||
|
|| info.BaseInfo.Contains("DOVI")
|
||||||
|
|| info.Type.Contains("dvvideo")
|
||||||
|
|| (DoViRegex().IsMatch(output) && info.Type == "Video")
|
||||||
|
)
|
||||||
|
info.DolbyVison = true;
|
||||||
|
|
||||||
|
if (StartRegex().IsMatch(output))
|
||||||
{
|
{
|
||||||
var info = new Mediainfo()
|
var f = StartRegex().Match(output).Groups[1].Value;
|
||||||
{
|
if (double.TryParse(f, out var d))
|
||||||
Text = TypeRegex().Match(stream.Value).Groups[2].Value.TrimEnd(),
|
info.StartTime = TimeSpan.FromSeconds(d);
|
||||||
Id = IdRegex().Match(stream.Value).Groups[1].Value,
|
|
||||||
Type = TypeRegex().Match(stream.Value).Groups[1].Value,
|
|
||||||
};
|
|
||||||
|
|
||||||
info.Resolution = ResRegex().Match(info.Text).Value;
|
|
||||||
info.Bitrate = BitrateRegex().Match(info.Text).Value;
|
|
||||||
info.Fps = FpsRegex().Match(info.Text).Value;
|
|
||||||
info.BaseInfo = BaseInfoRegex().Match(info.Text).Groups[1].Value;
|
|
||||||
info.BaseInfo = ReplaceRegex().Replace(info.BaseInfo, "");
|
|
||||||
info.HDR = info.Text.Contains("/bt2020/");
|
|
||||||
|
|
||||||
if (info.BaseInfo.Contains("dvhe")
|
|
||||||
|| info.BaseInfo.Contains("dvh1")
|
|
||||||
|| info.BaseInfo.Contains("DOVI")
|
|
||||||
|| info.Type.Contains("dvvideo")
|
|
||||||
|| (DoViRegex().IsMatch(output) && info.Type == "Video")
|
|
||||||
)
|
|
||||||
info.DolbyVison = true;
|
|
||||||
|
|
||||||
if (StartRegex().IsMatch(output))
|
|
||||||
{
|
|
||||||
var f = StartRegex().Match(output).Groups[1].Value;
|
|
||||||
if (double.TryParse(f, out var d))
|
|
||||||
info.StartTime = TimeSpan.FromSeconds(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.Count == 0)
|
result.Add(info);
|
||||||
{
|
|
||||||
result.Add(new Mediainfo()
|
|
||||||
{
|
|
||||||
Type = "Unknown"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.Count == 0)
|
||||||
|
{
|
||||||
|
result.Add(new Mediainfo
|
||||||
|
{
|
||||||
|
Type = "Unknown"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,298 +1,285 @@
|
|||||||
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>
|
||||||
|
/// <param name="files"></param>
|
||||||
|
/// <param name="outputFilePath"></param>
|
||||||
|
public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath)
|
||||||
{
|
{
|
||||||
/// <summary>
|
if (files.Length == 0) return;
|
||||||
/// 输入一堆已存在的文件,合并到新文件
|
if (files.Length == 1)
|
||||||
/// </summary>
|
|
||||||
/// <param name="files"></param>
|
|
||||||
/// <param name="outputFilePath"></param>
|
|
||||||
public static void CombineMultipleFilesIntoSingleFile(string[] files, string outputFilePath)
|
|
||||||
{
|
{
|
||||||
if (files.Length == 0) return;
|
FileInfo fi = new FileInfo(files[0]);
|
||||||
if (files.Length == 1)
|
fi.CopyTo(outputFilePath, true);
|
||||||
{
|
return;
|
||||||
FileInfo fi = new FileInfo(files[0]);
|
|
||||||
fi.CopyTo(outputFilePath, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(Path.GetDirectoryName(outputFilePath)))
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!);
|
|
||||||
|
|
||||||
string[] inputFilePaths = files;
|
|
||||||
using (var outputStream = File.Create(outputFilePath))
|
|
||||||
{
|
|
||||||
foreach (var inputFilePath in inputFilePaths)
|
|
||||||
{
|
|
||||||
if (inputFilePath == "")
|
|
||||||
continue;
|
|
||||||
using (var inputStream = File.OpenRead(inputFilePath))
|
|
||||||
{
|
|
||||||
inputStream.CopyTo(outputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int InvokeFFmpeg(string binary, string command, string workingDirectory)
|
if (!Directory.Exists(Path.GetDirectoryName(outputFilePath)))
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!);
|
||||||
|
|
||||||
|
var inputFilePaths = files;
|
||||||
|
using var outputStream = File.Create(outputFilePath);
|
||||||
|
foreach (var inputFilePath in inputFilePaths)
|
||||||
{
|
{
|
||||||
Logger.DebugMarkUp($"{binary}: {command}");
|
if (inputFilePath == "")
|
||||||
|
continue;
|
||||||
using var p = new Process();
|
using var inputStream = File.OpenRead(inputFilePath);
|
||||||
p.StartInfo = new ProcessStartInfo()
|
inputStream.CopyTo(outputStream);
|
||||||
{
|
|
||||||
WorkingDirectory = workingDirectory,
|
|
||||||
FileName = binary,
|
|
||||||
Arguments = command,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
};
|
|
||||||
p.ErrorDataReceived += (sendProcess, output) =>
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(output.Data))
|
|
||||||
{
|
|
||||||
Logger.WarnMarkUp($"[grey]{output.Data.EscapeMarkup()}[/]");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
p.Start();
|
|
||||||
p.BeginErrorReadLine();
|
|
||||||
p.WaitForExit();
|
|
||||||
return p.ExitCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string[] PartialCombineMultipleFiles(string[] files)
|
|
||||||
{
|
|
||||||
var newFiles = new List<string>();
|
|
||||||
int div = 0;
|
|
||||||
if (files.Length <= 90000)
|
|
||||||
div = 100;
|
|
||||||
else
|
|
||||||
div = 200;
|
|
||||||
|
|
||||||
string outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T");
|
|
||||||
int index = 0; //序号
|
|
||||||
|
|
||||||
//按照div的容量分割为小数组
|
|
||||||
string[][] li = Enumerable.Range(0, files.Count() / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray();
|
|
||||||
foreach (var items in li)
|
|
||||||
{
|
|
||||||
if (items.Count() == 0)
|
|
||||||
continue;
|
|
||||||
var output = outputName + index.ToString("0000") + ".ts";
|
|
||||||
CombineMultipleFilesIntoSingleFile(items, output);
|
|
||||||
newFiles.Add(output);
|
|
||||||
//合并后删除这些文件
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
File.Delete(item);
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newFiles.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, bool useAACFilter,
|
|
||||||
bool fastStart = false,
|
|
||||||
bool writeDate = true, bool useConcatDemuxer = false, string poster = "", string audioName = "", string title = "",
|
|
||||||
string copyright = "", string comment = "", string encodingTool = "", string recTime = "")
|
|
||||||
{
|
|
||||||
//改为绝对路径
|
|
||||||
outputPath = Path.GetFullPath(outputPath);
|
|
||||||
|
|
||||||
string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime;
|
|
||||||
|
|
||||||
StringBuilder command = new StringBuilder("-loglevel warning -nostdin ");
|
|
||||||
string ddpAudio = string.Empty;
|
|
||||||
string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic";
|
|
||||||
ddpAudio = (File.Exists($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") ? File.ReadAllText($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") : "");
|
|
||||||
if (!string.IsNullOrEmpty(ddpAudio)) useAACFilter = false;
|
|
||||||
|
|
||||||
if (useConcatDemuxer)
|
|
||||||
{
|
|
||||||
// 使用 concat demuxer合并
|
|
||||||
var text = string.Join(Environment.NewLine, files.Select(f => $"file '{f}'"));
|
|
||||||
var tempFile = Path.GetTempFileName();
|
|
||||||
File.WriteAllText(tempFile, text);
|
|
||||||
command.Append($" -f concat -safe 0 -i \"{tempFile}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
command.Append(" -i concat:\"");
|
|
||||||
foreach (string t in files)
|
|
||||||
{
|
|
||||||
command.Append(Path.GetFileName(t) + "|");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
switch (muxFormat.ToUpper())
|
|
||||||
{
|
|
||||||
case ("MP4"):
|
|
||||||
command.Append("\" " + (string.IsNullOrEmpty(poster) ? "" : "-i \"" + poster + "\""));
|
|
||||||
command.Append(" " + (string.IsNullOrEmpty(ddpAudio) ? "" : "-i \"" + ddpAudio + "\""));
|
|
||||||
command.Append(
|
|
||||||
$" -map 0:v? {(string.IsNullOrEmpty(ddpAudio) ? "-map 0:a?" : $"-map {(string.IsNullOrEmpty(poster) ? "1" : "2")}:a -map 0:a?")} -map 0:s? " + (string.IsNullOrEmpty(poster) ? "" : addPoster)
|
|
||||||
+ (writeDate ? " -metadata date=\"" + dateString + "\"" : "") +
|
|
||||||
" -metadata encoding_tool=\"" + encodingTool + "\" -metadata title=\"" + title +
|
|
||||||
"\" -metadata copyright=\"" + copyright + "\" -metadata comment=\"" + comment +
|
|
||||||
$"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} title=\"" + audioName + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler=\"" + audioName + "\" ");
|
|
||||||
command.Append(string.IsNullOrEmpty(ddpAudio) ? "" : " -metadata:s:a:0 title=\"DD+\" -metadata:s:a:0 handler=\"DD+\" ");
|
|
||||||
if (fastStart)
|
|
||||||
command.Append("-movflags +faststart");
|
|
||||||
command.Append(" -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mp4\"");
|
|
||||||
break;
|
|
||||||
case ("MKV"):
|
|
||||||
command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mkv\"");
|
|
||||||
break;
|
|
||||||
case ("FLV"):
|
|
||||||
command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".flv\"");
|
|
||||||
break;
|
|
||||||
case ("M4A"):
|
|
||||||
command.Append("\" -map 0 -c copy -f mp4 -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".m4a\"");
|
|
||||||
break;
|
|
||||||
case ("TS"):
|
|
||||||
command.Append("\" -map 0 -c copy -y -f mpegts -bsf:v h264_mp4toannexb \"" + outputPath + ".ts\"");
|
|
||||||
break;
|
|
||||||
case ("EAC3"):
|
|
||||||
command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".eac3\"");
|
|
||||||
break;
|
|
||||||
case ("AAC"):
|
|
||||||
command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".m4a\"");
|
|
||||||
break;
|
|
||||||
case ("AC3"):
|
|
||||||
command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".ac3\"");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var code = InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0])!);
|
|
||||||
|
|
||||||
return code == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath, bool mp4, bool dateinfo)
|
|
||||||
{
|
|
||||||
var ext = mp4 ? "mp4" : "mkv";
|
|
||||||
string dateString = DateTime.Now.ToString("o");
|
|
||||||
StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn ");
|
|
||||||
|
|
||||||
//INPUT
|
|
||||||
foreach (var item in files)
|
|
||||||
{
|
|
||||||
command.Append($" -i \"{item.FilePath}\" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
//MAP
|
|
||||||
for (int i = 0; i < files.Length; i++)
|
|
||||||
{
|
|
||||||
command.Append($" -map {i} ");
|
|
||||||
}
|
|
||||||
|
|
||||||
var srt = files.Any(x => x.FilePath.EndsWith(".srt"));
|
|
||||||
|
|
||||||
if (mp4)
|
|
||||||
command.Append($" -strict unofficial -c:a copy -c:v copy -c:s mov_text "); //mp4不支持vtt/srt字幕,必须转换格式
|
|
||||||
else
|
|
||||||
command.Append($" -strict unofficial -c:a copy -c:v copy -c:s {(srt ? "srt" : "webvtt")} ");
|
|
||||||
|
|
||||||
//CLEAN
|
|
||||||
command.Append(" -map_metadata -1 ");
|
|
||||||
|
|
||||||
//LANG and NAME
|
|
||||||
var streamIndex = 0;
|
|
||||||
for (int i = 0; i < files.Length; i++)
|
|
||||||
{
|
|
||||||
//转换语言代码
|
|
||||||
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
|
|
||||||
command.Append($" -metadata:s:{streamIndex} language=\"{files[i].LangCode ?? "und"}\" ");
|
|
||||||
if (!string.IsNullOrEmpty(files[i].Description))
|
|
||||||
{
|
|
||||||
command.Append($" -metadata:s:{streamIndex} title=\"{files[i].Description}\" ");
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* -metadata:s:xx标记的是 输出的第xx个流的metadata,
|
|
||||||
* 若输入文件存在不止一个流时,这里单纯使用files的index
|
|
||||||
* 就有可能出现metadata错位的情况,所以加了如下逻辑
|
|
||||||
*/
|
|
||||||
if (files[i].Mediainfos.Count > 0)
|
|
||||||
streamIndex += files[i].Mediainfos.Count;
|
|
||||||
else
|
|
||||||
streamIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var videoTracks = files.Where(x => x.MediaType != Common.Enum.MediaType.AUDIO && x.MediaType != Common.Enum.MediaType.SUBTITLES);
|
|
||||||
var audioTracks = 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 (subTracks.Any()) command.Append(" -disposition:s 0 ");
|
|
||||||
if (audioTracks.Any())
|
|
||||||
{
|
|
||||||
//音频除了第一个音轨 都不设置默认
|
|
||||||
command.Append(" -disposition:a:0 default ");
|
|
||||||
for (int i = 1; i < audioTracks.Count(); i++)
|
|
||||||
{
|
|
||||||
command.Append($" -disposition:a:{i} 0 ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateinfo) command.Append($" -metadata date=\"{dateString}\" ");
|
|
||||||
command.Append($" -ignore_unknown -copy_unknown ");
|
|
||||||
command.Append($" \"{outputPath}.{ext}\"");
|
|
||||||
|
|
||||||
var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory);
|
|
||||||
|
|
||||||
return code == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool MuxInputsByMkvmerge(string binary, OutputFile[] files, string outputPath)
|
|
||||||
{
|
|
||||||
StringBuilder command = new StringBuilder($"-q --output \"{outputPath}.mkv\" ");
|
|
||||||
|
|
||||||
command.Append(" --no-chapters ");
|
|
||||||
|
|
||||||
var dFlag = false;
|
|
||||||
|
|
||||||
//LANG and NAME
|
|
||||||
for (int i = 0; i < files.Length; i++)
|
|
||||||
{
|
|
||||||
//转换语言代码
|
|
||||||
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
|
|
||||||
command.Append($" --language 0:\"{files[i].LangCode ?? "und"}\" ");
|
|
||||||
//字幕都不设置默认
|
|
||||||
if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES)
|
|
||||||
command.Append($" --default-track 0:no ");
|
|
||||||
//音频除了第一个音轨 都不设置默认
|
|
||||||
if (files[i].MediaType == Common.Enum.MediaType.AUDIO)
|
|
||||||
{
|
|
||||||
if (dFlag)
|
|
||||||
command.Append($" --default-track 0:no ");
|
|
||||||
dFlag = true;
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrEmpty(files[i].Description))
|
|
||||||
command.Append($" --track-name 0:\"{files[i].Description}\" ");
|
|
||||||
command.Append($" \"{files[i].FilePath}\" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory);
|
|
||||||
|
|
||||||
return code == 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static int InvokeFFmpeg(string binary, string command, string workingDirectory)
|
||||||
|
{
|
||||||
|
Logger.DebugMarkUp($"{binary}: {command}");
|
||||||
|
|
||||||
|
using var p = new Process();
|
||||||
|
p.StartInfo = new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
FileName = binary,
|
||||||
|
Arguments = command,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
p.ErrorDataReceived += (sendProcess, output) =>
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(output.Data))
|
||||||
|
{
|
||||||
|
Logger.WarnMarkUp($"[grey]{output.Data.EscapeMarkup()}[/]");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
p.Start();
|
||||||
|
p.BeginErrorReadLine();
|
||||||
|
p.WaitForExit();
|
||||||
|
return p.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] PartialCombineMultipleFiles(string[] files)
|
||||||
|
{
|
||||||
|
var newFiles = new List<string>();
|
||||||
|
var div = files.Length <= 90000 ? 100 : 200;
|
||||||
|
|
||||||
|
var outputName = Path.Combine(Path.GetDirectoryName(files[0])!, "T");
|
||||||
|
var index = 0; // 序号
|
||||||
|
|
||||||
|
// 按照div的容量分割为小数组
|
||||||
|
var li = Enumerable.Range(0, files.Length / div + 1).Select(x => files.Skip(x * div).Take(div).ToArray()).ToArray();
|
||||||
|
foreach (var items in li)
|
||||||
|
{
|
||||||
|
if (items.Length == 0)
|
||||||
|
continue;
|
||||||
|
var output = outputName + index.ToString("0000") + ".ts";
|
||||||
|
CombineMultipleFilesIntoSingleFile(items, output);
|
||||||
|
newFiles.Add(output);
|
||||||
|
// 合并后删除这些文件
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
File.Delete(item);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFiles.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool MergeByFFmpeg(string binary, string[] files, string outputPath, string muxFormat, bool useAACFilter,
|
||||||
|
bool fastStart = false,
|
||||||
|
bool writeDate = true, bool useConcatDemuxer = false, string poster = "", string audioName = "", string title = "",
|
||||||
|
string copyright = "", string comment = "", string encodingTool = "", string recTime = "")
|
||||||
|
{
|
||||||
|
// 改为绝对路径
|
||||||
|
outputPath = Path.GetFullPath(outputPath);
|
||||||
|
|
||||||
|
string dateString = string.IsNullOrEmpty(recTime) ? DateTime.Now.ToString("o") : recTime;
|
||||||
|
|
||||||
|
StringBuilder command = new StringBuilder("-loglevel warning -nostdin ");
|
||||||
|
string ddpAudio = string.Empty;
|
||||||
|
string addPoster = "-map 1 -c:v:1 copy -disposition:v:1 attached_pic";
|
||||||
|
ddpAudio = (File.Exists($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") ? File.ReadAllText($"{Path.GetFileNameWithoutExtension(outputPath + ".mp4")}.txt") : "");
|
||||||
|
if (!string.IsNullOrEmpty(ddpAudio)) useAACFilter = false;
|
||||||
|
|
||||||
|
if (useConcatDemuxer)
|
||||||
|
{
|
||||||
|
// 使用 concat demuxer合并
|
||||||
|
var text = string.Join(Environment.NewLine, files.Select(f => $"file '{f}'"));
|
||||||
|
var tempFile = Path.GetTempFileName();
|
||||||
|
File.WriteAllText(tempFile, text);
|
||||||
|
command.Append($" -f concat -safe 0 -i \"{tempFile}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
command.Append(" -i concat:\"");
|
||||||
|
foreach (string t in files)
|
||||||
|
{
|
||||||
|
command.Append(Path.GetFileName(t) + "|");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
switch (muxFormat.ToUpper())
|
||||||
|
{
|
||||||
|
case ("MP4"):
|
||||||
|
command.Append("\" " + (string.IsNullOrEmpty(poster) ? "" : "-i \"" + poster + "\""));
|
||||||
|
command.Append(" " + (string.IsNullOrEmpty(ddpAudio) ? "" : "-i \"" + ddpAudio + "\""));
|
||||||
|
command.Append(
|
||||||
|
$" -map 0:v? {(string.IsNullOrEmpty(ddpAudio) ? "-map 0:a?" : $"-map {(string.IsNullOrEmpty(poster) ? "1" : "2")}:a -map 0:a?")} -map 0:s? " + (string.IsNullOrEmpty(poster) ? "" : addPoster)
|
||||||
|
+ (writeDate ? " -metadata date=\"" + dateString + "\"" : "") +
|
||||||
|
" -metadata encoding_tool=\"" + encodingTool + "\" -metadata title=\"" + title +
|
||||||
|
"\" -metadata copyright=\"" + copyright + "\" -metadata comment=\"" + comment +
|
||||||
|
$"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} title=\"" + audioName + $"\" -metadata:s:a:{(string.IsNullOrEmpty(ddpAudio) ? "0" : "1")} handler=\"" + audioName + "\" ");
|
||||||
|
command.Append(string.IsNullOrEmpty(ddpAudio) ? "" : " -metadata:s:a:0 title=\"DD+\" -metadata:s:a:0 handler=\"DD+\" ");
|
||||||
|
if (fastStart)
|
||||||
|
command.Append("-movflags +faststart");
|
||||||
|
command.Append(" -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mp4\"");
|
||||||
|
break;
|
||||||
|
case ("MKV"):
|
||||||
|
command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".mkv\"");
|
||||||
|
break;
|
||||||
|
case ("FLV"):
|
||||||
|
command.Append("\" -map 0 -c copy -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".flv\"");
|
||||||
|
break;
|
||||||
|
case ("M4A"):
|
||||||
|
command.Append("\" -map 0 -c copy -f mp4 -y " + (useAACFilter ? "-bsf:a aac_adtstoasc" : "") + " \"" + outputPath + ".m4a\"");
|
||||||
|
break;
|
||||||
|
case ("TS"):
|
||||||
|
command.Append("\" -map 0 -c copy -y -f mpegts -bsf:v h264_mp4toannexb \"" + outputPath + ".ts\"");
|
||||||
|
break;
|
||||||
|
case ("EAC3"):
|
||||||
|
command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".eac3\"");
|
||||||
|
break;
|
||||||
|
case ("AAC"):
|
||||||
|
command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".m4a\"");
|
||||||
|
break;
|
||||||
|
case ("AC3"):
|
||||||
|
command.Append("\" -map 0:a -c copy -y \"" + outputPath + ".ac3\"");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = InvokeFFmpeg(binary, command.ToString(), Path.GetDirectoryName(files[0])!);
|
||||||
|
|
||||||
|
return code == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool MuxInputsByFFmpeg(string binary, OutputFile[] files, string outputPath, MuxFormat muxFormat, bool dateinfo)
|
||||||
|
{
|
||||||
|
var ext = OtherUtil.GetMuxExtension(muxFormat);
|
||||||
|
string dateString = DateTime.Now.ToString("o");
|
||||||
|
StringBuilder command = new StringBuilder("-loglevel warning -nostdin -y -dn ");
|
||||||
|
|
||||||
|
// INPUT
|
||||||
|
foreach (var item in files)
|
||||||
|
{
|
||||||
|
command.Append($" -i \"{item.FilePath}\" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAP
|
||||||
|
for (int i = 0; i < files.Length; i++)
|
||||||
|
{
|
||||||
|
command.Append($" -map {i} ");
|
||||||
|
}
|
||||||
|
|
||||||
|
var srt = files.Any(x => x.FilePath.EndsWith(".srt"));
|
||||||
|
|
||||||
|
if (muxFormat == MuxFormat.MP4)
|
||||||
|
command.Append($" -strict unofficial -c:a copy -c:v copy -c:s mov_text "); // mp4不支持vtt/srt字幕,必须转换格式
|
||||||
|
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")} ");
|
||||||
|
else throw new ArgumentException($"unknown format: {muxFormat}");
|
||||||
|
|
||||||
|
// CLEAN
|
||||||
|
command.Append(" -map_metadata -1 ");
|
||||||
|
|
||||||
|
// LANG and NAME
|
||||||
|
var streamIndex = 0;
|
||||||
|
for (int i = 0; i < files.Length; i++)
|
||||||
|
{
|
||||||
|
// 转换语言代码
|
||||||
|
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
|
||||||
|
command.Append($" -metadata:s:{streamIndex} language=\"{files[i].LangCode ?? "und"}\" ");
|
||||||
|
if (!string.IsNullOrEmpty(files[i].Description))
|
||||||
|
{
|
||||||
|
command.Append($" -metadata:s:{streamIndex} title=\"{files[i].Description}\" ");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* -metadata:s:xx标记的是 输出的第xx个流的metadata,
|
||||||
|
* 若输入文件存在不止一个流时,这里单纯使用files的index
|
||||||
|
* 就有可能出现metadata错位的情况,所以加了如下逻辑
|
||||||
|
*/
|
||||||
|
if (files[i].Mediainfos.Count > 0)
|
||||||
|
streamIndex += files[i].Mediainfos.Count;
|
||||||
|
else
|
||||||
|
streamIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoTracks = files.Where(x => x.MediaType != Common.Enum.MediaType.AUDIO && x.MediaType != Common.Enum.MediaType.SUBTITLES);
|
||||||
|
var audioTracks = 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 (subTracks.Any()) command.Append(" -disposition:s 0 ");
|
||||||
|
if (audioTracks.Any())
|
||||||
|
{
|
||||||
|
// 音频除了第一个音轨 都不设置默认
|
||||||
|
command.Append(" -disposition:a:0 default ");
|
||||||
|
for (int i = 1; i < audioTracks.Count(); i++)
|
||||||
|
{
|
||||||
|
command.Append($" -disposition:a:{i} 0 ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateinfo) command.Append($" -metadata date=\"{dateString}\" ");
|
||||||
|
command.Append($" -ignore_unknown -copy_unknown ");
|
||||||
|
command.Append($" \"{outputPath}{ext}\"");
|
||||||
|
|
||||||
|
var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory);
|
||||||
|
|
||||||
|
return code == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool MuxInputsByMkvmerge(string binary, OutputFile[] files, string outputPath)
|
||||||
|
{
|
||||||
|
StringBuilder command = new StringBuilder($"-q --output \"{outputPath}.mkv\" ");
|
||||||
|
|
||||||
|
command.Append(" --no-chapters ");
|
||||||
|
|
||||||
|
var dFlag = false;
|
||||||
|
|
||||||
|
// LANG and NAME
|
||||||
|
for (int i = 0; i < files.Length; i++)
|
||||||
|
{
|
||||||
|
// 转换语言代码
|
||||||
|
LanguageCodeUtil.ConvertLangCodeAndDisplayName(files[i]);
|
||||||
|
command.Append($" --language 0:\"{files[i].LangCode ?? "und"}\" ");
|
||||||
|
// 字幕都不设置默认
|
||||||
|
if (files[i].MediaType == Common.Enum.MediaType.SUBTITLES)
|
||||||
|
command.Append($" --default-track 0:no ");
|
||||||
|
// 音频除了第一个音轨 都不设置默认
|
||||||
|
if (files[i].MediaType == Common.Enum.MediaType.AUDIO)
|
||||||
|
{
|
||||||
|
if (dFlag)
|
||||||
|
command.Append($" --default-track 0:no ");
|
||||||
|
dFlag = true;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(files[i].Description))
|
||||||
|
command.Append($" --track-name 0:\"{files[i].Description}\" ");
|
||||||
|
command.Append($" \"{files[i].FilePath}\" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = InvokeFFmpeg(binary, command.ToString(), Environment.CurrentDirectory);
|
||||||
|
|
||||||
|
return code == 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,169 +1,173 @@
|
|||||||
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();
|
||||||
|
if (headers == null) return dic;
|
||||||
|
|
||||||
|
foreach (string header in headers)
|
||||||
{
|
{
|
||||||
Dictionary<string, string> dic = new();
|
var index = header.IndexOf(':');
|
||||||
|
if (index != -1)
|
||||||
if (headers != null)
|
|
||||||
{
|
{
|
||||||
foreach (string header in headers)
|
dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim();
|
||||||
{
|
|
||||||
var index = header.IndexOf(':');
|
|
||||||
if (index != -1)
|
|
||||||
{
|
|
||||||
dic[header[..index].Trim().ToLower()] = header[(index + 1)..].Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
.Split(',').Select(s => (char)int.Parse(s)).ToArray();
|
|
||||||
public static string GetValidFileName(string input, string re = ".", bool filterSlash = false)
|
|
||||||
{
|
|
||||||
string title = input;
|
|
||||||
foreach (char invalidChar in InvalidChars)
|
|
||||||
{
|
|
||||||
title = title.Replace(invalidChar.ToString(), re);
|
|
||||||
}
|
|
||||||
if (filterSlash)
|
|
||||||
{
|
|
||||||
title = title.Replace("/", re);
|
|
||||||
title = title.Replace("\\", re);
|
|
||||||
}
|
|
||||||
return title.Trim('.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从输入自动获取文件名
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="input"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string GetFileNameFromInput(string input, bool addSuffix = true)
|
|
||||||
{
|
|
||||||
var saveName = addSuffix ? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") : string.Empty;
|
|
||||||
if (File.Exists(input))
|
|
||||||
{
|
|
||||||
saveName = Path.GetFileNameWithoutExtension(input) + "_" + saveName;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var uri = new Uri(input.Split('?').First());
|
|
||||||
var name = Path.GetFileNameWithoutExtension(uri.LocalPath);
|
|
||||||
saveName = GetValidFileName(name) + "_" + saveName;
|
|
||||||
}
|
|
||||||
return saveName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从 hh:mm:ss 解析TimeSpan
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="timeStr"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static TimeSpan ParseDur(string timeStr)
|
|
||||||
{
|
|
||||||
var arr = timeStr.Replace(":", ":").Split(':');
|
|
||||||
var days = -1;
|
|
||||||
var hours = -1;
|
|
||||||
var mins = -1;
|
|
||||||
var secs = -1;
|
|
||||||
arr.Reverse().Select(i => Convert.ToInt32(i)).ToList().ForEach(item =>
|
|
||||||
{
|
|
||||||
if (secs == -1) secs = item;
|
|
||||||
else if (mins == -1) mins = item;
|
|
||||||
else if (hours == -1) hours = item;
|
|
||||||
else if (days == -1) days = item;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (days == -1) days = 0;
|
|
||||||
if (hours == -1) hours = 0;
|
|
||||||
if (mins == -1) mins = 0;
|
|
||||||
if (secs == -1) secs = 0;
|
|
||||||
|
|
||||||
return new TimeSpan(days, hours, mins, secs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 从1h3m20s解析出总秒数
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="timeStr"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="ArgumentException"></exception>
|
|
||||||
public static double ParseSeconds(string timeStr)
|
|
||||||
{
|
|
||||||
var pattern = new Regex(@"^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$");
|
|
||||||
|
|
||||||
var match = pattern.Match(timeStr);
|
|
||||||
|
|
||||||
if (!match.Success)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("时间格式无效");
|
|
||||||
}
|
|
||||||
|
|
||||||
int hours = match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0;
|
|
||||||
int minutes = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0;
|
|
||||||
int seconds = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0;
|
|
||||||
|
|
||||||
return hours * 3600 + minutes * 60 + seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
//若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录
|
|
||||||
public static void SafeDeleteDir(string dirPath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var parent = Path.GetDirectoryName(dirPath)!;
|
|
||||||
if (!Directory.EnumerateFileSystemEntries(dirPath).Any())
|
|
||||||
{
|
|
||||||
Directory.Delete(dirPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SafeDeleteDir(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 解压并替换原文件
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filePath"></param>
|
|
||||||
public static async Task DeGzipFileAsync(string filePath)
|
|
||||||
{
|
|
||||||
string deGzipFile = Path.ChangeExtension(filePath, ".tmp");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var fileToDecompressAsStream = File.OpenRead(filePath))
|
|
||||||
{
|
|
||||||
using var decompressedStream = File.Create(deGzipFile);
|
|
||||||
using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress);
|
|
||||||
await decompressionStream.CopyToAsync(decompressedStream);
|
|
||||||
}
|
|
||||||
File.Delete(filePath);
|
|
||||||
File.Move(deGzipFile, filePath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (File.Exists(deGzipFile)) File.Delete(deGzipFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetEnvironmentVariable(string key, string defaultValue = "")
|
return dic;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
public static string GetValidFileName(string input, string re = "_", bool filterSlash = false)
|
||||||
|
{
|
||||||
|
var title = InvalidChars.Aggregate(input, (current, invalidChar) => current.Replace(invalidChar.ToString(), re));
|
||||||
|
if (filterSlash)
|
||||||
{
|
{
|
||||||
return Environment.GetEnvironmentVariable(key) ?? defaultValue;
|
title = title.Replace("/", re);
|
||||||
|
title = title.Replace("\\", re);
|
||||||
|
}
|
||||||
|
return title.Trim('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从输入自动获取文件名
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <param name="addSuffix"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string GetFileNameFromInput(string input, bool addSuffix = true)
|
||||||
|
{
|
||||||
|
var saveName = addSuffix ? DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss") : string.Empty;
|
||||||
|
if (File.Exists(input))
|
||||||
|
{
|
||||||
|
saveName = Path.GetFileNameWithoutExtension(input) + "_" + saveName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var uri = new Uri(input.Split('?').First());
|
||||||
|
var name = Path.GetFileNameWithoutExtension(uri.LocalPath);
|
||||||
|
saveName = GetValidFileName(name) + "_" + saveName;
|
||||||
|
}
|
||||||
|
return saveName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 hh:mm:ss 解析TimeSpan
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeStr"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static TimeSpan ParseDur(string timeStr)
|
||||||
|
{
|
||||||
|
var arr = timeStr.Replace(":", ":").Split(':');
|
||||||
|
var days = -1;
|
||||||
|
var hours = -1;
|
||||||
|
var mins = -1;
|
||||||
|
var secs = -1;
|
||||||
|
arr.Reverse().Select(i => Convert.ToInt32(i)).ToList().ForEach(item =>
|
||||||
|
{
|
||||||
|
if (secs == -1) secs = item;
|
||||||
|
else if (mins == -1) mins = item;
|
||||||
|
else if (hours == -1) hours = item;
|
||||||
|
else if (days == -1) days = item;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (days == -1) days = 0;
|
||||||
|
if (hours == -1) hours = 0;
|
||||||
|
if (mins == -1) mins = 0;
|
||||||
|
if (secs == -1) secs = 0;
|
||||||
|
|
||||||
|
return new TimeSpan(days, hours, mins, secs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从1h3m20s解析出总秒数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeStr"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="ArgumentException"></exception>
|
||||||
|
public static double ParseSeconds(string timeStr)
|
||||||
|
{
|
||||||
|
var pattern = TimeStrRegex();
|
||||||
|
|
||||||
|
var match = pattern.Match(timeStr);
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("时间格式无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
int hours = match.Groups[1].Success ? int.Parse(match.Groups[1].Value) : 0;
|
||||||
|
int minutes = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 0;
|
||||||
|
int seconds = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 0;
|
||||||
|
|
||||||
|
return hours * 3600 + minutes * 60 + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 若该文件夹为空,删除,同时判断其父文件夹,直到遇到根目录或不为空的目录
|
||||||
|
public static void SafeDeleteDir(string dirPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(dirPath) || !Directory.Exists(dirPath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parent = Path.GetDirectoryName(dirPath)!;
|
||||||
|
if (!Directory.EnumerateFileSystemEntries(dirPath).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(dirPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SafeDeleteDir(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解压并替换原文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath"></param>
|
||||||
|
public static async Task DeGzipFileAsync(string filePath)
|
||||||
|
{
|
||||||
|
var deGzipFile = Path.ChangeExtension(filePath, ".dezip_tmp");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using (var fileToDecompressAsStream = File.OpenRead(filePath))
|
||||||
|
{
|
||||||
|
await using var decompressedStream = File.Create(deGzipFile);
|
||||||
|
await using var decompressionStream = new GZipStream(fileToDecompressAsStream, CompressionMode.Decompress);
|
||||||
|
await decompressionStream.CopyToAsync(decompressedStream);
|
||||||
|
};
|
||||||
|
File.Delete(filePath);
|
||||||
|
File.Move(deGzipFile, filePath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (File.Exists(deGzipFile)) File.Delete(deGzipFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static string GetEnvironmentVariable(string key, string 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();
|
||||||
|
}
|
@ -1,113 +1,104 @@
|
|||||||
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())
|
||||||
|
{
|
||||||
|
return new NamedPipeServerStream(pipeName, PipeDirection.InOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), pipeName);
|
||||||
|
using var p = new Process();
|
||||||
|
p.StartInfo = new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
FileName = "mkfifo",
|
||||||
|
Arguments = path,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
};
|
||||||
|
p.Start();
|
||||||
|
p.WaitForExit();
|
||||||
|
Thread.Sleep(200);
|
||||||
|
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath)
|
||||||
|
{
|
||||||
|
return await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
return StartPipeMux(binary, pipeNames, outputPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool StartPipeMux(string binary, string[] pipeNames, string outputPath)
|
||||||
|
{
|
||||||
|
string dateString = DateTime.Now.ToString("o");
|
||||||
|
StringBuilder command = new StringBuilder("-y -fflags +genpts -loglevel quiet ");
|
||||||
|
|
||||||
|
string customDest = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_OPTIONS");
|
||||||
|
string pipeDir = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_TMP_DIR", Path.GetTempPath());
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(customDest))
|
||||||
|
{
|
||||||
|
command.Append(" -re ");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in pipeNames)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
command.Append($" -i \"\\\\.\\pipe\\{item}\" ");
|
||||||
return new NamedPipeServerStream(pipeName, PipeDirection.InOut);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
// command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" ");
|
||||||
var path = Path.Combine(Path.GetTempPath(), pipeName);
|
command.Append($" -i \"{Path.Combine(pipeDir, item)}\" ");
|
||||||
using var p = new Process();
|
|
||||||
p.StartInfo = new ProcessStartInfo()
|
|
||||||
{
|
|
||||||
FileName = "mkfifo",
|
|
||||||
Arguments = path,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
};
|
|
||||||
p.Start();
|
|
||||||
p.WaitForExit();
|
|
||||||
Thread.Sleep(200);
|
|
||||||
return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<bool> StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath)
|
for (int i = 0; i < pipeNames.Length; i++)
|
||||||
{
|
{
|
||||||
return await Task.Run(async () =>
|
command.Append($" -map {i} ");
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
return StartPipeMux(binary, pipeNames, outputPath);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool StartPipeMux(string binary, string[] pipeNames, string outputPath)
|
command.Append(" -strict unofficial -c copy ");
|
||||||
|
command.Append($" -metadata date=\"{dateString}\" ");
|
||||||
|
command.Append($" -ignore_unknown -copy_unknown ");
|
||||||
|
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(customDest))
|
||||||
{
|
{
|
||||||
string dateString = DateTime.Now.ToString("o");
|
if (customDest.Trim().StartsWith('-'))
|
||||||
StringBuilder command = new StringBuilder("-y -fflags +genpts -loglevel quiet ");
|
command.Append(customDest);
|
||||||
|
|
||||||
string customDest = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_OPTIONS");
|
|
||||||
string pipeDir = OtherUtil.GetEnvironmentVariable("RE_LIVE_PIPE_TMP_DIR", Path.GetTempPath());
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(customDest))
|
|
||||||
{
|
|
||||||
command.Append(" -re ");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in pipeNames)
|
|
||||||
{
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
command.Append($" -i \"\\\\.\\pipe\\{item}\" ");
|
|
||||||
else
|
|
||||||
//command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" ");
|
|
||||||
command.Append($" -i \"{Path.Combine(pipeDir, item)}\" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < pipeNames.Length; i++)
|
|
||||||
{
|
|
||||||
command.Append($" -map {i} ");
|
|
||||||
}
|
|
||||||
|
|
||||||
command.Append(" -strict unofficial -c copy ");
|
|
||||||
command.Append($" -metadata date=\"{dateString}\" ");
|
|
||||||
command.Append($" -ignore_unknown -copy_unknown ");
|
|
||||||
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(customDest))
|
|
||||||
{
|
|
||||||
if (customDest.Trim().StartsWith("-"))
|
|
||||||
command.Append(customDest);
|
|
||||||
else
|
|
||||||
command.Append($" -f mpegts -shortest \"{customDest}\"");
|
|
||||||
Logger.WarnMarkUp($"[deepskyblue1]{command.ToString().EscapeMarkup()}[/]");
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
command.Append($" -f mpegts -shortest \"{customDest}\"");
|
||||||
command.Append($" -f mpegts -shortest \"{outputPath}\"");
|
Logger.WarnMarkUp($"[deepskyblue1]{command.ToString().EscapeMarkup()}[/]");
|
||||||
}
|
|
||||||
|
|
||||||
using var p = new Process();
|
|
||||||
p.StartInfo = new ProcessStartInfo()
|
|
||||||
{
|
|
||||||
WorkingDirectory = Environment.CurrentDirectory,
|
|
||||||
FileName = binary,
|
|
||||||
Arguments = command.ToString(),
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
};
|
|
||||||
//p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42");
|
|
||||||
p.Start();
|
|
||||||
p.WaitForExit();
|
|
||||||
|
|
||||||
return p.ExitCode == 0;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
command.Append($" -f mpegts -shortest \"{outputPath}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var p = new Process();
|
||||||
|
p.StartInfo = new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
WorkingDirectory = Environment.CurrentDirectory,
|
||||||
|
FileName = binary,
|
||||||
|
Arguments = command.ToString(),
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
// p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42");
|
||||||
|
p.Start();
|
||||||
|
p.WaitForExit();
|
||||||
|
|
||||||
|
return p.ExitCode == 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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>
|
||||||
|
/// 写出图形字幕PNG文件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="finalVtt"></param>
|
||||||
|
/// <param name="tmpDir">临时目录</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir)
|
||||||
{
|
{
|
||||||
/// <summary>
|
if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::")))
|
||||||
/// 写出图形字幕PNG文件
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="finalVtt"></param>
|
|
||||||
/// <param name="tmpDir">临时目录</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<bool> TryWriteImagePngsAsync(WebVttSub? finalVtt, string tmpDir)
|
|
||||||
{
|
{
|
||||||
if (finalVtt != null && finalVtt.Cues.Any(v => v.Payload.StartsWith("Base64::")))
|
Logger.WarnMarkUp(ResString.processImageSub);
|
||||||
|
var i = 0;
|
||||||
|
foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::")))
|
||||||
{
|
{
|
||||||
Logger.WarnMarkUp(ResString.processImageSub);
|
var name = $"{i++}.png";
|
||||||
var _i = 0;
|
var dest = "";
|
||||||
foreach (var img in finalVtt.Cues.Where(v => v.Payload.StartsWith("Base64::")))
|
for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{i++}.png") ;
|
||||||
{
|
var base64 = img.Payload[8..];
|
||||||
var name = $"{_i++}.png";
|
await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64));
|
||||||
var dest = "";
|
img.Payload = name;
|
||||||
for (; File.Exists(dest = Path.Combine(tmpDir, name)); name = $"{_i++}.png") ;
|
|
||||||
var base64 = img.Payload[8..];
|
|
||||||
await File.WriteAllBytesAsync(dest, Convert.FromBase64String(base64));
|
|
||||||
img.Payload = name;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
else return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user