Document Get-VideoSubtitles.ps1 in README.md, index.md and SKILL.md, describing subtitle extraction and -List behavior, supported text and Blu-ray PGS formats, unsupported DVD and DVB bitmap tracks, and replacement of same-named output files in the current directory
This commit is contained in:
1 parent
fcc208e351
commit
dad95f0728
6 files changed
+208
-1
No files matched your search
@@ -0,0 +1,104 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts the embedded subtitle tracks of a video file into the current directory.
|
||||
.PARAMETER Video
|
||||
Path to the input video file. Can be absolute or relative to current directory.
|
||||
.PARAMETER List
|
||||
When present, lists the subtitle tracks instead of extracting them. Creates no files.
|
||||
.DESCRIPTION
|
||||
This script uses ffprobe to list the subtitle streams of a video file and ffmpeg to copy each one out:
|
||||
- Text subtitles (SubRip, WebVTT and mov_text, found in MKV, WebM, MP4, MOV and M4V) are written as .srt.
|
||||
- ASS/SSA subtitles (MKV) are written as .ass, keeping their styling.
|
||||
- Blu-ray PGS subtitles (hdmv_pgs_subtitle, found in MKV, M2TS and TS) are copied unchanged to .sup.
|
||||
- Bitmap DVD subtitles (dvd_subtitle, found in MKV and VOB) and DVB subtitles (dvb_subtitle, found in TS) are listed but not extracted, because ffmpeg cannot write them as text or as a standalone file.
|
||||
- AVI files have no standard subtitle track, so nothing is extracted from them.
|
||||
Bitmap subtitles can only be turned into text with OCR, which this script does not do.
|
||||
Each file is named "<video name>.<language>.<extension>" in the current directory. The language is the 2-letter ISO 639-1 code when one exists, otherwise the 3-letter code from the file, or "und" when the track has no language tag. A repeated name gets a counter: "<video name>.<language>.2.<extension>".
|
||||
An existing file with the same name is overwritten.
|
||||
.NOTES
|
||||
Requires ffmpeg and ffprobe to be installed and available in the system PATH.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Video,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$List
|
||||
)
|
||||
|
||||
function Get-LanguageCode([string]$Tag) {
|
||||
if (-not $Tag -or $Tag -eq 'und') { return 'und' }
|
||||
$culture = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::NeutralCultures) | Where-Object { $_.ThreeLetterISOLanguageName -eq $Tag } | Select-Object -First 1
|
||||
if ($culture) { return $culture.TwoLetterISOLanguageName }
|
||||
return $Tag
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $Video)) {
|
||||
throw "Video file not found: $Video"
|
||||
}
|
||||
$videoPath = (Resolve-Path -LiteralPath $Video).Path
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($videoPath)
|
||||
$outputDir = (Get-Location).ProviderPath
|
||||
|
||||
$ffprobeJson = & ffprobe -v error -select_streams s -show_entries 'stream=index,codec_name:stream_tags=language' -of json -- $videoPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed to read video file: $videoPath"
|
||||
}
|
||||
$streams = @((ConvertFrom-Json ($ffprobeJson -join "`n")).streams | Where-Object { $_ })
|
||||
|
||||
$plan = @()
|
||||
$used = @{}
|
||||
foreach ($s in $streams) {
|
||||
$ext = switch ($s.codec_name) {
|
||||
{ $_ -in 'subrip', 'mov_text', 'webvtt' } { '.srt' }
|
||||
{ $_ -in 'ass', 'ssa' } { '.ass' }
|
||||
'hdmv_pgs_subtitle' { '.sup' }
|
||||
default { $null }
|
||||
}
|
||||
$code = Get-LanguageCode $s.tags.language
|
||||
$fileName = "$baseName.$code$ext"
|
||||
$counter = 1
|
||||
while ($used.ContainsKey($fileName.ToLowerInvariant())) {
|
||||
$counter++
|
||||
$fileName = "$baseName.$code.$counter$ext"
|
||||
}
|
||||
$used[$fileName.ToLowerInvariant()] = $true
|
||||
$plan += [pscustomobject]@{ Index = [int]$s.index; Codec = $s.codec_name; Ext = $ext; File = Join-Path $outputDir $fileName }
|
||||
}
|
||||
|
||||
Write-Host "Subtitle tracks in: $videoPath"
|
||||
if ($streams.Count -eq 0) {
|
||||
Write-Host "No subtitle stream found in video file." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($List) {
|
||||
$plan | Format-Table -Property @{ Label = 'Index'; Expression = { $_.Index } }, @{ Label = 'Codec'; Expression = { $_.Codec } }, @{ Label = 'Output'; Expression = { if ($_.Ext) { Split-Path -Leaf $_.File } else { 'not supported' } } } -AutoSize | Out-String | Write-Host
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($p in $plan) {
|
||||
if (-not $p.Ext) {
|
||||
Write-Warning "Subtitle stream $($p.Index) ($($p.Codec)) is not supported and was skipped."
|
||||
continue
|
||||
}
|
||||
if ($PSCmdlet.ShouldProcess($p.File, "Extract subtitle stream $($p.Index) ($($p.Codec))")) {
|
||||
$codecArgs = if ($p.Ext -eq '.srt') { @('-c:s', 'srt') } else { @('-c:s', 'copy') }
|
||||
if ($p.Ext -eq '.sup') { $codecArgs += @('-f', 'sup') }
|
||||
& ffmpeg -v error -y -i $videoPath -map "0:$($p.Index)" @codecArgs $p.File
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffmpeg failed to extract subtitle stream $($p.Index) with exit code $LASTEXITCODE"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $p.File)) {
|
||||
throw "Subtitle file was not created: $($p.File)"
|
||||
}
|
||||
Write-Host "Subtitle extracted: $($p.File)" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Error: $_"
|
||||
exit 1
|
||||
}
|
||||
Reference in new issue
Block a user