code.pablomurad.com

fast-scripts

28 double-click Windows batch tools for video, audio and files

first commit
author
Pablo Murad
date
commit
1fffbaba5e7e56b64d1082b038429d835dcfdaa1
changes
4 files +667 −0
@@ -0,0 +1,50 @@+# Sistema / editor
+Thumbs.db
+Desktop.ini
+.DS_Store
+*.swp
+*.tmp
+*~
+.vscode/
+.idea/
+
+# Segredos e ambiente (por precaução)
+.env
+.env.*
+*.pem
+*.key
+*.pfx
+credentials.json
+secrets.*
+
+# Saídas dos scripts (se rodarem dentro do repo)
+Convertido/
+Transcodificado/
+
+# Mídia que possa cair na pasta por acidente
+*.jpg
+*.jpeg
+*.png
+*.webp
+*.bmp
+*.gif
+*.tif
+*.tiff
+*.heic
+*.heif
+*.avif
+*.mp4
+*.mkv
+*.avi
+*.mov
+*.wmv
+*.flv
+*.webm
+*.m4v
+*.mpg
+*.mpeg
+*.mts
+*.m2ts
+*.ts
+*.3gp
+*.vob
@@ -0,0 +1,326 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Converter Imagens - redimensiona/converte em lote (ImageMagick)
+#  Arquivo único .cmd (auto-executável como PowerShell)
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Converter Imagens' } catch {}
+
+function Pausar {
+    Write-Host ''
+    Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray
+    [void][System.Console]::ReadLine()
+}
+
+function Titulo($texto) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $texto" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+
+# ------------------------------------------------------------
+# Configuração persistente (lembra últimas escolhas)
+# ------------------------------------------------------------
+$configDir  = Join-Path $env:APPDATA 'ConverterImagens'
+$configFile = Join-Path $configDir 'config.json'
+
+function Carregar-Config {
+    if (Test-Path -LiteralPath $configFile) {
+        try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {}
+    }
+    return $null
+}
+
+function Salvar-Config($entrada, $formato, $modo, $valor) {
+    try {
+        New-Item -ItemType Directory -Path $configDir -Force | Out-Null
+        [pscustomobject]@{
+            UltimaEntrada   = $entrada
+            UltimoFormato   = $formato
+            UltimoModo      = $modo
+            UltimoValor     = $valor
+        } | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8
+    } catch {}
+}
+
+# Formatos de imagem reconhecidos na entrada
+$extensoes = @(
+    '.jpg', '.jpeg', '.png', '.webp', '.bmp',
+    '.gif', '.tif', '.tiff', '.heic', '.heif', '.avif'
+)
+
+# Formatos de saída oferecidos
+$formatos = @(
+    [pscustomobject]@{ Id=1; Nome='Manter formato original'; Desc='Só redimensiona, mantém o tipo do arquivo'; Ext='' },
+    [pscustomobject]@{ Id=2; Nome='JPG';  Desc='Fotos, menor tamanho (sem transparência)'; Ext='.jpg'  },
+    [pscustomobject]@{ Id=3; Nome='PNG';  Desc='Sem perdas, mantém transparência';         Ext='.png'  },
+    [pscustomobject]@{ Id=4; Nome='WebP'; Desc='Ótima compressão, qualidade alta';         Ext='.webp' }
+)
+
+# ------------------------------------------------------------
+# 1) Verificação de dependências
+# ------------------------------------------------------------
+Titulo 'Converter Imagens'
+Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+
+$magick = Get-Command magick -ErrorAction SilentlyContinue
+if (-not $magick) {
+    Write-Host ''
+    Write-Host '[ERRO] O ImageMagick não foi encontrado no sistema.' -ForegroundColor Red
+    Write-Host 'Ele é necessário para converter as imagens.' -ForegroundColor Yellow
+    Write-Host ''
+    Write-Host 'Para instalar, abra o Terminal / Prompt e rode:' -ForegroundColor Gray
+    Write-Host '    winget install ImageMagick.ImageMagick' -ForegroundColor White
+    Write-Host ''
+    Write-Host 'Depois feche e abra este programa novamente.' -ForegroundColor Gray
+    Pausar
+    exit 1
+}
+Write-Host "[OK] ImageMagick encontrado: $($magick.Source)" -ForegroundColor Green
+
+$config = Carregar-Config
+
+# ------------------------------------------------------------
+# 2) Escolha da pasta OU de uma imagem única
+# ------------------------------------------------------------
+$entradaPadrao = Join-Path $env:USERPROFILE 'Downloads\Imagens'
+$entradaAtual  = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { $entradaPadrao }
+
+$modoArquivo  = $false
+$arquivoUnico = $null
+
+while ($true) {
+    Titulo 'Pasta ou imagem'
+    Write-Host 'Caminho atual:' -ForegroundColor Gray
+    Write-Host "  $entradaAtual" -ForegroundColor White
+    Write-Host ''
+    Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou uma IMAGEM' -ForegroundColor DarkGray
+    $resp = Read-Host 'Pasta/imagem'
+
+    if (-not [string]::IsNullOrWhiteSpace($resp)) {
+        $entradaAtual = $resp.Trim().Trim('"')
+    }
+
+    if (Test-Path -LiteralPath $entradaAtual -PathType Leaf) {
+        $ext = [System.IO.Path]::GetExtension($entradaAtual).ToLowerInvariant()
+        if ($extensoes -notcontains $ext) {
+            Write-Host ''
+            Write-Host "[!] Esse arquivo não é uma imagem suportada ($ext)." -ForegroundColor Yellow
+            continue
+        }
+        $modoArquivo  = $true
+        $arquivoUnico = (Resolve-Path -LiteralPath $entradaAtual).Path
+        break
+    }
+    elseif (Test-Path -LiteralPath $entradaAtual -PathType Container) {
+        $modoArquivo = $false
+        break
+    }
+
+    Write-Host ''
+    Write-Host "[!] Caminho não encontrado: $entradaAtual" -ForegroundColor Yellow
+    $criar = Read-Host 'Criar como pasta? (s/N)'
+    if ($criar -match '^(s|sim|y|yes)$') {
+        try {
+            New-Item -ItemType Directory -Path $entradaAtual -Force | Out-Null
+            Write-Host "[OK] Pasta criada." -ForegroundColor Green
+            $modoArquivo = $false
+            break
+        } catch {
+            Write-Host "[ERRO] Não foi possível criar a pasta." -ForegroundColor Red
+        }
+    }
+}
+
+if ($modoArquivo) {
+    $arquivoObj = Get-Item -LiteralPath $arquivoUnico
+    $origem     = $arquivoObj.DirectoryName
+} else {
+    $origem     = (Resolve-Path -LiteralPath $entradaAtual).Path
+}
+$destino = Join-Path $origem 'Convertido'
+
+# ------------------------------------------------------------
+# 3) Formato de saída
+# ------------------------------------------------------------
+$formatoPadrao = if ($config -and $config.UltimoFormato) { [int]$config.UltimoFormato } else { 1 }
+
+Titulo 'Formato de saída'
+foreach ($fmt in $formatos) {
+    $marca = if ($fmt.Id -eq $formatoPadrao) { '>' } else { ' ' }
+    $cor   = if ($fmt.Id -eq $formatoPadrao) { 'White' } else { 'Gray' }
+    Write-Host ("{0} [{1}] {2}" -f $marca, $fmt.Id, $fmt.Nome) -ForegroundColor $cor
+    Write-Host ("       {0}" -f $fmt.Desc) -ForegroundColor DarkGray
+}
+Write-Host ''
+Write-Host "Enter = usar a opção padrão [$formatoPadrao]" -ForegroundColor DarkGray
+
+$formato = $null
+while (-not $formato) {
+    $escolha = Read-Host 'Formato (1-4)'
+    if ([string]::IsNullOrWhiteSpace($escolha)) { $escolha = "$formatoPadrao" }
+    $formato = $formatos | Where-Object { $_.Id -eq ($escolha -as [int]) } | Select-Object -First 1
+    if (-not $formato) { Write-Host '[!] Escolha inválida. Digite um número de 1 a 4.' -ForegroundColor Yellow }
+}
+
+# ------------------------------------------------------------
+# 4) Tamanho: px (maior lado), % ou não redimensionar
+# ------------------------------------------------------------
+$modoPadrao = if ($config -and $config.UltimoModo) { $config.UltimoModo } else { 'px' }
+
+Titulo 'Tamanho'
+Write-Host '  [1] Pixels do maior lado   (ex.: 1920 - a imagem cabe em 1920x1920)' -ForegroundColor Gray
+Write-Host '  [2] Porcentagem (%)        (ex.: 50 = metade do tamanho)' -ForegroundColor Gray
+Write-Host '  [3] Não redimensionar      (só converter o formato)' -ForegroundColor Gray
+Write-Host ''
+$rotuloPadrao = switch ($modoPadrao) { 'px' {'1'} 'pct' {'2'} 'none' {'3'} default {'1'} }
+Write-Host "Enter = usar o modo padrão [$rotuloPadrao]" -ForegroundColor DarkGray
+
+$modo = $null
+while (-not $modo) {
+    $op = Read-Host 'Modo (1-3)'
+    if ([string]::IsNullOrWhiteSpace($op)) { $op = $rotuloPadrao }
+    switch ($op) {
+        '1' { $modo = 'px' }
+        '2' { $modo = 'pct' }
+        '3' { $modo = 'none' }
+        default { Write-Host '[!] Escolha inválida. Digite 1, 2 ou 3.' -ForegroundColor Yellow }
+    }
+}
+
+$valor      = $null
+$resizeArg  = $null
+$resumoTam  = ''
+
+if ($modo -eq 'px') {
+    $valorPadrao = if ($config -and $config.UltimoModo -eq 'px' -and $config.UltimoValor) { $config.UltimoValor } else { '' }
+    while (-not $valor) {
+        $dica = if ($valorPadrao) { " (Enter = $valorPadrao)" } else { '' }
+        $ent = Read-Host "Maior lado em pixels$dica"
+        if ([string]::IsNullOrWhiteSpace($ent) -and $valorPadrao) { $ent = "$valorPadrao" }
+        $n = $ent -as [int]
+        if ($n -and $n -gt 0) { $valor = $n } else { Write-Host '[!] Digite um número de pixels válido (ex.: 1920).' -ForegroundColor Yellow }
+    }
+    # ">" garante proporcional e NUNCA amplia
+    $resizeArg = "$($valor)x$($valor)>"
+    $resumoTam = "maior lado $valor px"
+}
+elseif ($modo -eq 'pct') {
+    $valorPadrao = if ($config -and $config.UltimoModo -eq 'pct' -and $config.UltimoValor) { $config.UltimoValor } else { '' }
+    while (-not $valor) {
+        $dica = if ($valorPadrao) { " (Enter = $valorPadrao)" } else { '' }
+        $ent = Read-Host "Porcentagem %$dica"
+        if ([string]::IsNullOrWhiteSpace($ent) -and $valorPadrao) { $ent = "$valorPadrao" }
+        $p = ($ent -replace '%','') -as [double]
+        if ($p -and $p -gt 0) { $valor = $p } else { Write-Host '[!] Digite uma porcentagem válida (ex.: 50).' -ForegroundColor Yellow }
+    }
+    $resizeArg = "$valor%"
+    $resumoTam = "$valor%"
+}
+else {
+    $resumoTam = 'sem redimensionar'
+}
+
+Salvar-Config $origem $formato.Id $modo $valor
+
+# ------------------------------------------------------------
+# 5) Coleta das imagens
+# ------------------------------------------------------------
+if ($modoArquivo) {
+    $arquivos = @($arquivoObj)
+} else {
+    $arquivos = @(Get-ChildItem -LiteralPath $origem -Recurse -File -ErrorAction SilentlyContinue |
+        Where-Object {
+            ($extensoes -contains $_.Extension.ToLowerInvariant()) -and
+            (-not $_.FullName.StartsWith($destino, [System.StringComparison]::OrdinalIgnoreCase))
+        })
+}
+
+Titulo 'Processando'
+Write-Host "Pasta:    $origem"
+Write-Host "Formato:  $($formato.Nome)"
+Write-Host "Tamanho:  $resumoTam"
+Write-Host "Destino:  $destino"
+Write-Host "Imagens encontradas: $($arquivos.Count)" -ForegroundColor Cyan
+
+if ($arquivos.Count -eq 0) {
+    Write-Host ''
+    Write-Host 'Nenhuma imagem para converter nesse local.' -ForegroundColor Yellow
+    Pausar
+    exit 0
+}
+
+New-Item -ItemType Directory -Path $destino -Force | Out-Null
+
+$convertidos = 0
+$ignorados   = 0
+$erros       = 0
+$indice      = 0
+
+foreach ($arquivo in $arquivos) {
+    $indice++
+
+    $relativo = $arquivo.FullName.Substring($origem.Length).TrimStart('\')
+    $subpasta = Split-Path -Path $relativo -Parent
+    $pastaSaida = if ([string]::IsNullOrWhiteSpace($subpasta)) { $destino } else { Join-Path $destino $subpasta }
+    New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null
+
+    # Extensão de saída: mantém a original ou usa a do formato escolhido
+    $extSaida = if ([string]::IsNullOrEmpty($formato.Ext)) { $arquivo.Extension.ToLowerInvariant() } else { $formato.Ext }
+    $saida = Join-Path $pastaSaida ($arquivo.BaseName + $extSaida)
+
+    Write-Host ''
+    Write-Host ("[{0}/{1}]" -f $indice, $arquivos.Count) -ForegroundColor Magenta -NoNewline
+    Write-Host " $($arquivo.Name)"
+
+    if (Test-Path -LiteralPath $saida) {
+        Write-Host "   [IGNORADO] Já existe na pasta de destino." -ForegroundColor Yellow
+        $ignorados++
+        continue
+    }
+
+    # Entrada: para GIF/multiframe indo para formato de imagem única, pega o 1o quadro
+    $entradaMagick = $arquivo.FullName
+    if ($arquivo.Extension.ToLowerInvariant() -eq '.gif' -and $extSaida -ne '.gif') {
+        $entradaMagick = "$($arquivo.FullName)[0]"
+    }
+
+    $imgArgs = @($entradaMagick)
+    if ($resizeArg) { $imgArgs += @('-resize', $resizeArg) }
+    # JPG não tem transparência: achata sobre fundo branco
+    if ($extSaida -eq '.jpg' -or $extSaida -eq '.jpeg') {
+        $imgArgs += @('-background', 'white', '-alpha', 'remove', '-alpha', 'off')
+    }
+    if ($extSaida -in '.jpg', '.jpeg', '.webp') {
+        $imgArgs += @('-quality', '85')
+    }
+    $imgArgs += $saida
+
+    & magick @imgArgs
+
+    if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) {
+        Write-Host "   [OK] Convertida." -ForegroundColor Green
+        $convertidos++
+    } else {
+        Write-Host "   [ERRO] Falha ao converter." -ForegroundColor Red
+        if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }
+        $erros++
+    }
+}
+
+# ------------------------------------------------------------
+# 6) Resumo final
+# ------------------------------------------------------------
+Titulo 'Concluído'
+Write-Host "Convertidas: $convertidos" -ForegroundColor Green
+Write-Host "Ignoradas:   $ignorados"  -ForegroundColor Yellow
+Write-Host "Erros:       $erros"       -ForegroundColor $(if ($erros -gt 0) { 'Red' } else { 'Gray' })
+Write-Host "Destino:     $destino"
+Pausar
@@ -0,0 +1,25 @@+# fast-scripts
+
+Scripts rápidos para Windows, em lote e com interface no terminal.
+
+![PowerShell](https://img.shields.io/badge/PowerShell-5391FE?style=flat&logo=powershell&logoColor=white)
+![Batch](https://img.shields.io/badge/Batch-.cmd-4D4D4D?style=flat)
+![Windows](https://img.shields.io/badge/Windows-0078D6?style=flat&logo=windows&logoColor=white)
+
+## Programas
+
+### Converter Imagens
+
+`Imagens/converter-imagens.cmd`
+
+Redimensiona e converte imagens em lote com ImageMagick. Aceita uma pasta ou um arquivo, escolhe formato (JPG, PNG, WebP ou original) e tamanho (pixels, porcentagem ou só converter). A saída vai para a pasta `Convertido`.
+
+### Transcodificar
+
+`Vídeos/transcodificar.cmd`
+
+Converte vídeos em lote com FFmpeg para MP4 (H.264 + AAC). Aceita uma pasta ou um arquivo e oferece perfis de qualidade (da máxima à compacta). A saída vai para a pasta `Transcodificado`.
+
+---
+
+**Pablo Murad** — pablomurad[at]pm[dot]me
@@ -0,0 +1,266 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Transcodificar - conversor de vídeos em lote (FFmpeg)
+#  Arquivo único .cmd (auto-executável como PowerShell)
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Transcodificar Vídeos' } catch {}
+
+function Pausar {
+    Write-Host ''
+    Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray
+    [void][System.Console]::ReadLine()
+}
+
+function Titulo($texto) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $texto" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+
+# ------------------------------------------------------------
+# Configuração persistente (lembra última pasta e qualidade)
+# ------------------------------------------------------------
+$configDir  = Join-Path $env:APPDATA 'Transcodificar'
+$configFile = Join-Path $configDir 'config.json'
+
+function Carregar-Config {
+    if (Test-Path -LiteralPath $configFile) {
+        try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {}
+    }
+    return $null
+}
+
+function Salvar-Config($pasta, $qualidade) {
+    try {
+        New-Item -ItemType Directory -Path $configDir -Force | Out-Null
+        [pscustomobject]@{ UltimaPasta = $pasta; UltimaQualidade = $qualidade } |
+            ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8
+    } catch {}
+}
+
+# ------------------------------------------------------------
+# Perfis de qualidade (o menor/compacto é o último - padrão)
+# ------------------------------------------------------------
+$perfis = @(
+    [pscustomobject]@{ Id=1; Nome='Máxima (resolução original)'; Desc='Mantém a resolução, altíssima qualidade';     Altura=0;    Crf=18; Audio='192k' },
+    [pscustomobject]@{ Id=2; Nome='Alta (1080p)';                Desc='Full HD, ótima qualidade';                     Altura=1080; Crf=20; Audio='160k' },
+    [pscustomobject]@{ Id=3; Nome='Média (720p)';                Desc='HD, bom equilíbrio tamanho x qualidade';       Altura=720;  Crf=23; Audio='128k' },
+    [pscustomobject]@{ Id=4; Nome='Baixa (480p)';                Desc='Arquivo leve, qualidade razoável';             Altura=480;  Crf=26; Audio='96k'  },
+    [pscustomobject]@{ Id=5; Nome='Mínima (compacta ~320p)';     Desc='Bem pequena, para prévia / upload rápido';     Altura=320;  Crf=30; Audio='64k'  }
+)
+
+# Formatos de vídeo reconhecidos
+$extensoes = @(
+    '.mp4', '.mkv', '.avi', '.mov', '.wmv',
+    '.flv', '.webm', '.m4v', '.mpg', '.mpeg',
+    '.mts', '.m2ts', '.ts', '.3gp', '.vob'
+)
+
+# ------------------------------------------------------------
+# 1) Verificação de dependências
+# ------------------------------------------------------------
+Titulo 'Transcodificar Vídeos'
+Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+
+$ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue
+if (-not $ffmpeg) {
+    Write-Host ''
+    Write-Host '[ERRO] O FFmpeg não foi encontrado no sistema.' -ForegroundColor Red
+    Write-Host 'Ele é necessário para converter os vídeos.' -ForegroundColor Yellow
+    Write-Host ''
+    Write-Host 'Para instalar, abra o Terminal / Prompt e rode:' -ForegroundColor Gray
+    Write-Host '    winget install Gyan.FFmpeg' -ForegroundColor White
+    Write-Host ''
+    Write-Host 'Depois feche e abra este programa novamente.' -ForegroundColor Gray
+    Pausar
+    exit 1
+}
+Write-Host "[OK] FFmpeg encontrado: $($ffmpeg.Source)" -ForegroundColor Green
+
+$config = Carregar-Config
+
+# ------------------------------------------------------------
+# 2) Escolha da pasta OU de um vídeo único (lembra o último)
+# ------------------------------------------------------------
+$entradaPadrao = Join-Path $env:USERPROFILE 'Downloads\Vídeos'
+$entradaAtual  = if ($config -and $config.UltimaPasta) { $config.UltimaPasta } else { $entradaPadrao }
+
+$modoArquivo  = $false
+$arquivoUnico = $null
+
+while ($true) {
+    Titulo 'Pasta ou vídeo'
+    Write-Host 'Caminho atual:' -ForegroundColor Gray
+    Write-Host "  $entradaAtual" -ForegroundColor White
+    Write-Host ''
+    Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO de vídeo' -ForegroundColor DarkGray
+    $resp = Read-Host 'Pasta/arquivo'
+
+    if (-not [string]::IsNullOrWhiteSpace($resp)) {
+        $entradaAtual = $resp.Trim().Trim('"')
+    }
+
+    if (Test-Path -LiteralPath $entradaAtual -PathType Leaf) {
+        $ext = [System.IO.Path]::GetExtension($entradaAtual).ToLowerInvariant()
+        if ($extensoes -notcontains $ext) {
+            Write-Host ''
+            Write-Host "[!] Esse arquivo não é um vídeo suportado ($ext)." -ForegroundColor Yellow
+            continue
+        }
+        $modoArquivo  = $true
+        $arquivoUnico = (Resolve-Path -LiteralPath $entradaAtual).Path
+        break
+    }
+    elseif (Test-Path -LiteralPath $entradaAtual -PathType Container) {
+        $modoArquivo = $false
+        break
+    }
+
+    Write-Host ''
+    Write-Host "[!] Caminho não encontrado: $entradaAtual" -ForegroundColor Yellow
+    $criar = Read-Host 'Criar como pasta? (s/N)'
+    if ($criar -match '^(s|sim|y|yes)$') {
+        try {
+            New-Item -ItemType Directory -Path $entradaAtual -Force | Out-Null
+            Write-Host "[OK] Pasta criada." -ForegroundColor Green
+            $modoArquivo = $false
+            break
+        } catch {
+            Write-Host "[ERRO] Não foi possível criar a pasta." -ForegroundColor Red
+        }
+    }
+}
+
+if ($modoArquivo) {
+    $arquivoObj = Get-Item -LiteralPath $arquivoUnico
+    $origem     = $arquivoObj.DirectoryName
+} else {
+    $origem     = (Resolve-Path -LiteralPath $entradaAtual).Path
+}
+$destino = Join-Path $origem 'Transcodificado'
+
+# ------------------------------------------------------------
+# 3) Escolha da qualidade
+# ------------------------------------------------------------
+$qualidadePadrao = if ($config -and $config.UltimaQualidade) { [int]$config.UltimaQualidade } else { 5 }
+
+Titulo 'Qualidade de saída'
+foreach ($p in $perfis) {
+    $marca = if ($p.Id -eq $qualidadePadrao) { '>' } else { ' ' }
+    $cor   = if ($p.Id -eq $qualidadePadrao) { 'White' } else { 'Gray' }
+    Write-Host ("{0} [{1}] {2}" -f $marca, $p.Id, $p.Nome) -ForegroundColor $cor
+    Write-Host ("       {0}" -f $p.Desc) -ForegroundColor DarkGray
+}
+Write-Host ''
+Write-Host "Enter = usar a opção padrão [$qualidadePadrao]" -ForegroundColor DarkGray
+
+$perfil = $null
+while (-not $perfil) {
+    $escolha = Read-Host 'Qualidade (1-5)'
+    if ([string]::IsNullOrWhiteSpace($escolha)) { $escolha = "$qualidadePadrao" }
+    $perfil = $perfis | Where-Object { $_.Id -eq ($escolha -as [int]) } | Select-Object -First 1
+    if (-not $perfil) { Write-Host '[!] Escolha inválida. Digite um número de 1 a 5.' -ForegroundColor Yellow }
+}
+
+Salvar-Config $origem $perfil.Id
+
+# ------------------------------------------------------------
+# 4) Coleta dos arquivos de vídeo
+# ------------------------------------------------------------
+if ($modoArquivo) {
+    $arquivos = @($arquivoObj)
+} else {
+    $arquivos = @(Get-ChildItem -LiteralPath $origem -Recurse -File -ErrorAction SilentlyContinue |
+        Where-Object {
+            ($extensoes -contains $_.Extension.ToLowerInvariant()) -and
+            (-not $_.FullName.StartsWith($destino, [System.StringComparison]::OrdinalIgnoreCase))
+        })
+}
+
+Titulo 'Processando'
+Write-Host "Pasta:     $origem"
+Write-Host "Qualidade: $($perfil.Nome)  (CRF $($perfil.Crf), áudio $($perfil.Audio))"
+Write-Host "Destino:   $destino"
+Write-Host "Vídeos encontrados: $($arquivos.Count)" -ForegroundColor Cyan
+
+if ($arquivos.Count -eq 0) {
+    Write-Host ''
+    Write-Host 'Nenhum vídeo para converter nessa pasta.' -ForegroundColor Yellow
+    Pausar
+    exit 0
+}
+
+New-Item -ItemType Directory -Path $destino -Force | Out-Null
+
+# Filtro de escala conforme o perfil (nunca faz upscaling)
+if ($perfil.Altura -eq 0) {
+    $vf = 'scale=trunc(iw/2)*2:trunc(ih/2)*2'
+} else {
+    $vf = "scale=-2:trunc(min($($perfil.Altura)\,ih)/2)*2"
+}
+
+$convertidos = 0
+$ignorados   = 0
+$erros       = 0
+$indice      = 0
+
+foreach ($arquivo in $arquivos) {
+    $indice++
+
+    $relativo = $arquivo.FullName.Substring($origem.Length).TrimStart('\')
+    $subpasta = Split-Path -Path $relativo -Parent
+    $pastaSaida = if ([string]::IsNullOrWhiteSpace($subpasta)) { $destino } else { Join-Path $destino $subpasta }
+    New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null
+
+    $saida = Join-Path $pastaSaida ($arquivo.BaseName + '.mp4')
+
+    Write-Host ''
+    Write-Host ("[{0}/{1}]" -f $indice, $arquivos.Count) -ForegroundColor Magenta -NoNewline
+    Write-Host " $($arquivo.Name)"
+
+    if (Test-Path -LiteralPath $saida) {
+        Write-Host "   [IGNORADO] Já existe na pasta de destino." -ForegroundColor Yellow
+        $ignorados++
+        continue
+    }
+
+    $ffArgs = @(
+        '-hide_banner', '-nostdin',
+        '-i', $arquivo.FullName,
+        '-map', '0:v:0', '-map', '0:a?',
+        '-vf', $vf,
+        '-c:v', 'libx264', '-preset', 'medium', '-crf', "$($perfil.Crf)", '-pix_fmt', 'yuv420p',
+        '-c:a', 'aac', '-b:a', $perfil.Audio, '-ac', '2',
+        '-movflags', '+faststart',
+        '-y', $saida
+    )
+
+    & ffmpeg @ffArgs
+
+    if ($LASTEXITCODE -eq 0) {
+        Write-Host "   [OK] Convertido." -ForegroundColor Green
+        $convertidos++
+    } else {
+        Write-Host "   [ERRO] Falha ao converter." -ForegroundColor Red
+        if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }
+        $erros++
+    }
+}
+
+# ------------------------------------------------------------
+# 5) Resumo final
+# ------------------------------------------------------------
+Titulo 'Concluído'
+Write-Host "Convertidos: $convertidos" -ForegroundColor Green
+Write-Host "Ignorados:   $ignorados"  -ForegroundColor Yellow
+Write-Host "Erros:       $erros"       -ForegroundColor $(if ($erros -gt 0) { 'Red' } else { 'Gray' })
+Write-Host "Destino:     $destino"
+Pausar