需要修改 RootDir 和 FfmpegBin指定到相应的目录,然后powershell下执行脚本。
bash
param(
[string]$RootDir = 'C:\Users\guest\Videos\bilibili',
[string]$FfmpegBin = 'C:\ffmpeg-n8.1.1-11-ge4c7fbf6c0-win64-lgpl-8.1\bin',
[switch]$Force,
[switch]$WhatIf
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ffmpeg = Join-Path $FfmpegBin 'ffmpeg.exe'
$ffprobe = Join-Path $FfmpegBin 'ffprobe.exe'
if (-not (Test-Path $RootDir)) {
throw "Root directory does not exist: $RootDir"
}
if (-not (Test-Path $ffmpeg)) {
throw "ffmpeg not found: $ffmpeg"
}
if (-not (Test-Path $ffprobe)) {
throw "ffprobe not found: $ffprobe"
}
function Get-SafeFileName {
param([string]$Name)
if ([string]::IsNullOrWhiteSpace($Name)) {
return 'output'
}
$invalid = [System.IO.Path]::GetInvalidFileNameChars()
$safe = -join ($Name.ToCharArray() | ForEach-Object {
if ($invalid -contains $_) { '_' } else { $_ }
})
$safe = $safe.Trim()
if ([string]::IsNullOrWhiteSpace($safe)) {
return 'output'
}
return $safe
}
function Needs-PrefixStrip {
param([string]$FilePath)
$fs = [System.IO.File]::OpenRead($FilePath)
try {
if ($fs.Length -lt 14) {
return $false
}
$buf = New-Object byte[] 14
$read = $fs.Read($buf, 0, 14)
if ($read -lt 14) {
return $false
}
# Some bilibili m4s files prepend 9 ASCII '0' bytes before normal fMP4 header.
for ($i = 0; $i -lt 9; $i++) {
if ($buf[$i] -ne 0x30) {
return $false
}
}
return ($buf[13] -eq 0x66) # 'f' in 'ftyp'
}
finally {
$fs.Dispose()
}
}
function Prepare-M4sFile {
param(
[string]$InputPath,
[string]$WorkDir
)
if (-not (Needs-PrefixStrip -FilePath $InputPath)) {
return $InputPath
}
$tmpName = [System.IO.Path]::GetFileNameWithoutExtension($InputPath) + '.clean.m4s'
$tmpPath = Join-Path $WorkDir $tmpName
$bytes = [System.IO.File]::ReadAllBytes($InputPath)
if ($bytes.Length -le 9) {
throw "File too short to strip prefix: $InputPath"
}
[System.IO.File]::WriteAllBytes($tmpPath, $bytes[9..($bytes.Length - 1)])
return $tmpPath
}
function Get-CodecType {
param([string]$FilePath)
$result = & $ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of default=nokey=1:noprint_wrappers=1 $FilePath
if ($LASTEXITCODE -eq 0 -and ($result -join '') -match 'video') {
return 'video'
}
$result = & $ffprobe -v error -select_streams a:0 -show_entries stream=codec_type -of default=nokey=1:noprint_wrappers=1 $FilePath
if ($LASTEXITCODE -eq 0 -and ($result -join '') -match 'audio') {
return 'audio'
}
return 'unknown'
}
function Get-OutputBaseName {
param([string]$FolderPath)
$infoPath = Join-Path $FolderPath 'videoInfo.json'
if (Test-Path $infoPath) {
try {
$raw = Get-Content -Path $infoPath -Raw -Encoding UTF8
$obj = $raw | ConvertFrom-Json
if (-not [string]::IsNullOrWhiteSpace($obj.tabName)) {
return (Get-SafeFileName -Name $obj.tabName)
}
if (-not [string]::IsNullOrWhiteSpace($obj.title)) {
return (Get-SafeFileName -Name $obj.title)
}
}
catch {
# Ignore parse errors and fall back to folder name.
}
}
return (Get-SafeFileName -Name ([System.IO.Path]::GetFileName($FolderPath)))
}
$dirs = Get-ChildItem -Path $RootDir -Directory | Sort-Object Name
if ($dirs.Count -eq 0) {
Write-Host "No subdirectories found under: $RootDir"
exit 0
}
$ok = 0
$skip = 0
$fail = 0
foreach ($dir in $dirs) {
try {
Write-Host "`n==> Processing: $($dir.FullName)"
$m4s = Get-ChildItem -Path $dir.FullName -File -Filter '*.m4s' |
Where-Object { $_.Name -notlike '*.clean.m4s' } |
Sort-Object Length -Descending
if ($m4s.Count -lt 2) {
Write-Host " Skip: less than 2 m4s files"
$skip++
continue
}
$tempFiles = New-Object System.Collections.Generic.List[string]
$prepared = @()
foreach ($f in $m4s) {
$p = Prepare-M4sFile -InputPath $f.FullName -WorkDir $dir.FullName
if ($p -ne $f.FullName) {
$tempFiles.Add($p)
}
$prepared += [PSCustomObject]@{
Source = $f.FullName
Path = $p
Kind = (Get-CodecType -FilePath $p)
}
}
$video = $prepared | Where-Object { $_.Kind -eq 'video' } | Select-Object -First 1
$audio = $prepared | Where-Object { $_.Kind -eq 'audio' } | Select-Object -First 1
if (-not $video -or -not $audio) {
Write-Host " Skip: cannot identify both video and audio streams"
$skip++
foreach ($tmp in $tempFiles) {
if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
}
continue
}
$base = Get-OutputBaseName -FolderPath $dir.FullName
$outPath = Join-Path $dir.FullName ($base + '.mp4')
if ((Test-Path $outPath) -and (-not $Force)) {
Write-Host " Skip: output exists ($outPath). Use -Force to overwrite."
$skip++
foreach ($tmp in $tempFiles) {
if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
}
continue
}
if ($WhatIf) {
Write-Host " WhatIf: would merge"
Write-Host " video: $($video.Path)"
Write-Host " audio: $($audio.Path)"
Write-Host " output: $outPath"
$ok++
foreach ($tmp in $tempFiles) {
if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
}
continue
}
& $ffmpeg -y -i $video.Path -i $audio.Path -c copy -map 0:v:0 -map 1:a:0 $outPath | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "ffmpeg failed with code $LASTEXITCODE"
}
foreach ($tmp in $tempFiles) {
if (Test-Path $tmp) { Remove-Item -Path $tmp -Force }
}
Write-Host " OK: $outPath"
$ok++
}
catch {
Write-Host " Fail: $($_.Exception.Message)"
$fail++
}
}
Write-Host "`nDone. Success: $ok, Skipped: $skip, Failed: $fail"