510 lines
13 KiB
PowerShell
510 lines
13 KiB
PowerShell
#requires -Version 5.1
|
||
|
||
<#
|
||
Конвертация всех FLAC-файлов в CD-качество:
|
||
16 бит / 44,1 кГц / стерео.
|
||
|
||
Скрипт:
|
||
- рекурсивно ищет все *.flac;
|
||
- пропускает файлы, которые уже 16 бит / 44100 Гц / стерео;
|
||
- сохраняет теги и встроенную обложку;
|
||
- проверяет результат через ffprobe;
|
||
- после успешной проверки заменяет исходный файл;
|
||
- показывает прогресс;
|
||
- в конце выводит список файлов с ошибками.
|
||
#>
|
||
|
||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||
$ToolsFolder = Join-Path $ProjectRoot "tools"
|
||
$RootFolder = Join-Path $ProjectRoot "downloads"
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
function Find-Tool {
|
||
param(
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$CommandName,
|
||
|
||
[Parameter(Mandatory = $true)]
|
||
[string[]]$LocalNames
|
||
)
|
||
|
||
foreach ($LocalName in $LocalNames) {
|
||
$LocalPath = Join-Path $ToolsFolder $LocalName
|
||
if (Test-Path -LiteralPath $LocalPath -PathType Leaf) {
|
||
return (Resolve-Path -LiteralPath $LocalPath).Path
|
||
}
|
||
}
|
||
|
||
$Command = Get-Command $CommandName -ErrorAction SilentlyContinue
|
||
if ($Command) {
|
||
return $Command.Source
|
||
}
|
||
|
||
return $null
|
||
}
|
||
|
||
$FfmpegPath = Find-Tool -CommandName "ffmpeg.exe" -LocalNames @("ffmpeg.exe")
|
||
$FfprobePath = Find-Tool -CommandName "ffprobe.exe" -LocalNames @("ffprobe.exe")
|
||
|
||
|
||
try {
|
||
$Host.UI.RawUI.WindowTitle = "FLAC -> CD quality"
|
||
}
|
||
catch {
|
||
}
|
||
|
||
function Wait-BeforeExit {
|
||
Write-Host ""
|
||
[void](Read-Host "Нажмите Enter для выхода")
|
||
}
|
||
|
||
function Get-FlacAudioInfo {
|
||
param(
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$Path
|
||
)
|
||
|
||
$probeOutput = & $FfprobePath `
|
||
-v error `
|
||
-select_streams a:0 `
|
||
-show_entries "stream=codec_name,sample_rate,channels,bits_per_sample,bits_per_raw_sample,sample_fmt" `
|
||
-of json `
|
||
-- `
|
||
$Path 2>&1
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "ffprobe не смог прочитать файл:`n$probeOutput"
|
||
}
|
||
|
||
$probeData = $probeOutput | ConvertFrom-Json
|
||
|
||
if (
|
||
-not $probeData.streams -or
|
||
$probeData.streams.Count -lt 1
|
||
) {
|
||
throw "Аудиопоток не найден."
|
||
}
|
||
|
||
$stream = $probeData.streams[0]
|
||
$bitDepth = 0
|
||
|
||
if (
|
||
$null -ne $stream.bits_per_raw_sample -and
|
||
[string]$stream.bits_per_raw_sample -ne "" -and
|
||
[int]$stream.bits_per_raw_sample -gt 0
|
||
) {
|
||
$bitDepth = [int]$stream.bits_per_raw_sample
|
||
}
|
||
elseif (
|
||
$null -ne $stream.bits_per_sample -and
|
||
[string]$stream.bits_per_sample -ne "" -and
|
||
[int]$stream.bits_per_sample -gt 0
|
||
) {
|
||
$bitDepth = [int]$stream.bits_per_sample
|
||
}
|
||
elseif ([string]$stream.sample_fmt -match "s16") {
|
||
$bitDepth = 16
|
||
}
|
||
elseif ([string]$stream.sample_fmt -match "s32") {
|
||
$bitDepth = 24
|
||
}
|
||
|
||
[PSCustomObject]@{
|
||
Codec = [string]$stream.codec_name
|
||
SampleRate = [int]$stream.sample_rate
|
||
Channels = [int]$stream.channels
|
||
BitDepth = [int]$bitDepth
|
||
SampleFmt = [string]$stream.sample_fmt
|
||
}
|
||
}
|
||
|
||
Clear-Host
|
||
|
||
Write-Host "FLAC -> CD quality" -ForegroundColor Cyan
|
||
Write-Host "16 бит / 44,1 кГц / стерео"
|
||
Write-Host "Папка: $RootFolder"
|
||
Write-Host ""
|
||
|
||
if (
|
||
-not (
|
||
Test-Path `
|
||
-LiteralPath $RootFolder `
|
||
-PathType Container
|
||
)
|
||
) {
|
||
Write-Host "ОШИБКА: папка не найдена:" -ForegroundColor Red
|
||
Write-Host $RootFolder -ForegroundColor Red
|
||
|
||
Wait-BeforeExit
|
||
exit 1
|
||
}
|
||
|
||
if (-not $FfmpegPath) {
|
||
Write-Host "ОШИБКА: ffmpeg.exe не найден." -ForegroundColor Red
|
||
Write-Host "Проверены папка tools и системный PATH."
|
||
Wait-BeforeExit
|
||
exit 1
|
||
}
|
||
|
||
if (-not $FfprobePath) {
|
||
Write-Host "ОШИБКА: ffprobe.exe не найден." -ForegroundColor Red
|
||
Write-Host "Проверены папка tools и системный PATH."
|
||
Wait-BeforeExit
|
||
exit 1
|
||
}
|
||
|
||
$files = @(
|
||
Get-ChildItem `
|
||
-LiteralPath $RootFolder `
|
||
-Filter "*.flac" `
|
||
-File `
|
||
-Recurse |
|
||
Where-Object {
|
||
$_.Name -notlike "*.__cd_temp__.flac"
|
||
} |
|
||
Sort-Object FullName
|
||
)
|
||
|
||
$total = $files.Count
|
||
|
||
if ($total -eq 0) {
|
||
Write-Host "FLAC-файлы не найдены." `
|
||
-ForegroundColor Yellow
|
||
|
||
Wait-BeforeExit
|
||
exit 0
|
||
}
|
||
|
||
$converted = 0
|
||
$skipped = 0
|
||
$failed = 0
|
||
$index = 0
|
||
$startTime = Get-Date
|
||
|
||
$failedFiles = [System.Collections.Generic.List[object]]::new()
|
||
|
||
foreach ($file in $files) {
|
||
$index++
|
||
|
||
$percent = [int][Math]::Floor(
|
||
(($index - 1) / $total) * 100
|
||
)
|
||
|
||
Write-Progress `
|
||
-Activity "Конвертация FLAC в CD-качество" `
|
||
-Status (
|
||
"Файл {0} из {1}: {2}" -f `
|
||
$index,
|
||
$total,
|
||
$file.Name
|
||
) `
|
||
-CurrentOperation $file.DirectoryName `
|
||
-PercentComplete $percent
|
||
|
||
Write-Host ""
|
||
|
||
Write-Host (
|
||
"[{0}/{1}] {2}" -f `
|
||
$index,
|
||
$total,
|
||
$file.FullName
|
||
) -ForegroundColor White
|
||
|
||
$tempFile = Join-Path `
|
||
-Path $file.DirectoryName `
|
||
-ChildPath (
|
||
$file.BaseName +
|
||
".__cd_temp__.flac"
|
||
)
|
||
|
||
$backupFile = Join-Path `
|
||
-Path $file.DirectoryName `
|
||
-ChildPath (
|
||
$file.Name +
|
||
".__original_backup__"
|
||
)
|
||
|
||
try {
|
||
if (Test-Path -LiteralPath $tempFile) {
|
||
Remove-Item `
|
||
-LiteralPath $tempFile `
|
||
-Force
|
||
}
|
||
|
||
if (Test-Path -LiteralPath $backupFile) {
|
||
Remove-Item `
|
||
-LiteralPath $backupFile `
|
||
-Force
|
||
}
|
||
|
||
$sourceInfo = Get-FlacAudioInfo `
|
||
-Path $file.FullName
|
||
|
||
Write-Host (
|
||
"Исходник: {0} бит, {1} Гц, {2} канал(а)" -f `
|
||
$sourceInfo.BitDepth,
|
||
$sourceInfo.SampleRate,
|
||
$sourceInfo.Channels
|
||
)
|
||
|
||
$alreadyCdQuality = (
|
||
$sourceInfo.BitDepth -eq 16 -and
|
||
$sourceInfo.SampleRate -eq 44100 -and
|
||
$sourceInfo.Channels -eq 2
|
||
)
|
||
|
||
if ($alreadyCdQuality) {
|
||
$skipped++
|
||
|
||
Write-Host `
|
||
"Уже CD-качество — пропущено." `
|
||
-ForegroundColor Green
|
||
|
||
continue
|
||
}
|
||
|
||
Write-Host "Конвертация..." `
|
||
-ForegroundColor Yellow
|
||
|
||
$ffmpegOutput = & $FfmpegPath `
|
||
-hide_banner `
|
||
-loglevel error `
|
||
-y `
|
||
-i `
|
||
$file.FullName `
|
||
-map 0:a:0 `
|
||
-map "0:v?" `
|
||
-map_metadata 0 `
|
||
-c:a flac `
|
||
-sample_fmt s16 `
|
||
-ar 44100 `
|
||
-ac 2 `
|
||
-compression_level 8 `
|
||
-c:v copy `
|
||
-- `
|
||
$tempFile 2>&1
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "ffmpeg завершился с ошибкой:`n$ffmpegOutput"
|
||
}
|
||
|
||
if (
|
||
-not (
|
||
Test-Path `
|
||
-LiteralPath $tempFile `
|
||
-PathType Leaf
|
||
)
|
||
) {
|
||
throw "Временный файл не был создан."
|
||
}
|
||
|
||
$tempFileInfo = Get-Item `
|
||
-LiteralPath $tempFile
|
||
|
||
if ($tempFileInfo.Length -le 0) {
|
||
throw "Создан пустой временный файл."
|
||
}
|
||
|
||
$resultInfo = Get-FlacAudioInfo `
|
||
-Path $tempFile
|
||
|
||
$validResult = (
|
||
$resultInfo.BitDepth -eq 16 -and
|
||
$resultInfo.SampleRate -eq 44100 -and
|
||
$resultInfo.Channels -eq 2
|
||
)
|
||
|
||
if (-not $validResult) {
|
||
throw (
|
||
"Проверка результата не пройдена. " +
|
||
"Получено: {0} бит, {1} Гц, " +
|
||
"{2} канал(а)." -f `
|
||
$resultInfo.BitDepth,
|
||
$resultInfo.SampleRate,
|
||
$resultInfo.Channels
|
||
)
|
||
}
|
||
|
||
$originalCreationTime = $file.CreationTime
|
||
$originalLastWriteTime = $file.LastWriteTime
|
||
|
||
Move-Item `
|
||
-LiteralPath $file.FullName `
|
||
-Destination $backupFile
|
||
|
||
try {
|
||
Move-Item `
|
||
-LiteralPath $tempFile `
|
||
-Destination $file.FullName
|
||
|
||
$newFile = Get-Item `
|
||
-LiteralPath $file.FullName
|
||
|
||
$newFile.CreationTime = $originalCreationTime
|
||
$newFile.LastWriteTime = $originalLastWriteTime
|
||
|
||
Remove-Item `
|
||
-LiteralPath $backupFile `
|
||
-Force
|
||
}
|
||
catch {
|
||
if (
|
||
Test-Path `
|
||
-LiteralPath $file.FullName
|
||
) {
|
||
Remove-Item `
|
||
-LiteralPath $file.FullName `
|
||
-Force `
|
||
-ErrorAction SilentlyContinue
|
||
}
|
||
|
||
if (
|
||
Test-Path `
|
||
-LiteralPath $backupFile
|
||
) {
|
||
Move-Item `
|
||
-LiteralPath $backupFile `
|
||
-Destination $file.FullName `
|
||
-ErrorAction SilentlyContinue
|
||
}
|
||
|
||
throw
|
||
}
|
||
|
||
$converted++
|
||
|
||
Write-Host `
|
||
"Готово — исходный файл заменён." `
|
||
-ForegroundColor Cyan
|
||
}
|
||
catch {
|
||
$failed++
|
||
|
||
$errorMessage = $_.Exception.Message
|
||
|
||
$failedFiles.Add(
|
||
[PSCustomObject]@{
|
||
Name = $file.BaseName
|
||
FileName = $file.Name
|
||
FullName = $file.FullName
|
||
Directory = $file.DirectoryName
|
||
Error = $errorMessage
|
||
}
|
||
)
|
||
|
||
Write-Host "ОШИБКА:" `
|
||
-ForegroundColor Red
|
||
|
||
Write-Host $errorMessage `
|
||
-ForegroundColor Red
|
||
|
||
if (
|
||
Test-Path `
|
||
-LiteralPath $tempFile
|
||
) {
|
||
Remove-Item `
|
||
-LiteralPath $tempFile `
|
||
-Force `
|
||
-ErrorAction SilentlyContinue
|
||
}
|
||
|
||
if (
|
||
(Test-Path -LiteralPath $backupFile) -and
|
||
-not (
|
||
Test-Path `
|
||
-LiteralPath $file.FullName
|
||
)
|
||
) {
|
||
Move-Item `
|
||
-LiteralPath $backupFile `
|
||
-Destination $file.FullName `
|
||
-ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
}
|
||
|
||
Write-Progress `
|
||
-Activity "Конвертация FLAC в CD-качество" `
|
||
-Completed
|
||
|
||
$elapsed = (Get-Date) - $startTime
|
||
|
||
Write-Host ""
|
||
Write-Host "========================================" `
|
||
-ForegroundColor DarkGray
|
||
|
||
Write-Host "Обработка завершена" `
|
||
-ForegroundColor Cyan
|
||
|
||
Write-Host (
|
||
"Всего файлов: {0}" -f $total
|
||
)
|
||
|
||
Write-Host (
|
||
"Конвертировано: {0}" -f $converted
|
||
) -ForegroundColor Cyan
|
||
|
||
Write-Host (
|
||
"Пропущено (уже CD): {0}" -f $skipped
|
||
) -ForegroundColor Green
|
||
|
||
if ($failed -gt 0) {
|
||
Write-Host (
|
||
"Ошибок: {0}" -f $failed
|
||
) -ForegroundColor Red
|
||
}
|
||
else {
|
||
Write-Host "Ошибок: 0" `
|
||
-ForegroundColor Gray
|
||
}
|
||
|
||
Write-Host (
|
||
"Время: {0:hh\:mm\:ss}" -f $elapsed
|
||
)
|
||
|
||
Write-Host "========================================" `
|
||
-ForegroundColor DarkGray
|
||
|
||
if ($failedFiles.Count -gt 0) {
|
||
Write-Host ""
|
||
Write-Host "ТРЕКИ С ОШИБКАМИ:" `
|
||
-ForegroundColor Red
|
||
|
||
Write-Host "========================================" `
|
||
-ForegroundColor DarkGray
|
||
|
||
$failedNumber = 0
|
||
|
||
foreach ($failedFile in $failedFiles) {
|
||
$failedNumber++
|
||
|
||
Write-Host (
|
||
"{0}. {1}" -f `
|
||
$failedNumber,
|
||
$failedFile.Name
|
||
) -ForegroundColor Red
|
||
|
||
Write-Host (
|
||
" Файл: {0}" -f `
|
||
$failedFile.FullName
|
||
) -ForegroundColor DarkGray
|
||
|
||
Write-Host (
|
||
" Ошибка: {0}" -f `
|
||
$failedFile.Error
|
||
) -ForegroundColor DarkYellow
|
||
|
||
Write-Host ""
|
||
}
|
||
|
||
Write-Host "========================================" `
|
||
-ForegroundColor DarkGray
|
||
}
|
||
else {
|
||
Write-Host ""
|
||
Write-Host "Все файлы обработаны без ошибок." `
|
||
-ForegroundColor Green
|
||
}
|
||
|
||
Wait-BeforeExit |