more stuff
jump to
@@ -0,0 +1,356 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Comprimir PDF - reduz o tamanho de PDFs (Ghostscript) +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Comprimir PDF' } catch {} + +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'ComprimirPdf' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Comprimir PDF' +# O Ghostscript costuma NAO estar no PATH: procura tambem onde ele se instala. +$gsExe = $null +foreach ($n in @('gswin64c','gswin32c')) { + $c = Get-Command $n -ErrorAction SilentlyContinue + if ($c) { $gsExe = $c.Source; break } +} +if (-not $gsExe) { + foreach ($base in @("$env:ProgramFiles\gs", "${env:ProgramFiles(x86)}\gs")) { + if (-not (Test-Path -LiteralPath $base)) { continue } + $achado = Get-ChildItem -LiteralPath $base -Recurse -Filter 'gswin*c.exe' -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending | Select-Object -First 1 + if ($achado) { $gsExe = $achado.FullName; break } + } +} +if (-not $gsExe) { + Write-Host '' + Write-Host '[ERRO] Ghostscript não foi encontrado (é ele que comprime o PDF).' -ForegroundColor Red + Write-Host 'Instale com: winget install ArtifexSoftware.GhostScript' -ForegroundColor White + Pausar; exit 1 +} +Write-Host ("[OK] Ghostscript: {0}" -f $gsExe) -ForegroundColor Green +$config = Carregar-Config + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$ent = Pedir-Entrada $padrao @('.pdf') 'Pasta ou PDF' +$itens = Listar $ent @('.pdf') +if ($itens.Count -eq 0) { Write-Host 'Nenhum PDF encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } + +$niveis = @( + [pscustomobject]@{ Id=1; Nome='Tela - menor arquivo (72 dpi)'; V='/screen' }, + [pscustomobject]@{ Id=2; Nome='E-book - equilibrado (150 dpi)'; V='/ebook' }, + [pscustomobject]@{ Id=3; Nome='Impressão - boa qualidade (300 dpi)'; V='/printer' }, + [pscustomobject]@{ Id=4; Nome='Gráfica - máxima qualidade'; V='/prepress' } +) +$idPadrao = if ($config -and $config.UltimoNivel) { [int]$config.UltimoNivel } else { 2 } +Titulo 'Nível de compressão' +foreach ($n in $niveis) { + $m = if ($n.Id -eq $idPadrao) { '>' } else { ' ' } + $cor = if ($n.Id -eq $idPadrao) { 'White' } else { 'Gray' } + Write-Host ("{0} [{1}] {2}" -f $m, $n.Id, $n.Nome) -ForegroundColor $cor +} +Write-Host "Enter = usar a opção padrão [$idPadrao]" -ForegroundColor DarkGray +$nivel = $null +while (-not $nivel) { + $e = Read-Host 'Nível (1-4)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = "$idPadrao" } + $nivel = $niveis | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1 + if (-not $nivel) { Write-Host '[!] Escolha de 1 a 4.' -ForegroundColor Yellow } +} + +$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'Comprimido' }) +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimoNivel=$nivel.Id; UltimaSaida=$destino }) + +$ok=0;$ign=0;$err=0 +$tarefas = @(); $antes = @{} +foreach ($f in $itens) { + $saida = Join-Path $destino $f.Name + if (Test-Path -LiteralPath $saida) { $ign++; continue } + $antes[$saida] = $f.Length + $a = @('-sDEVICE=pdfwrite','-dCompatibilityLevel=1.4',"-dPDFSETTINGS=$($nivel.V)", + '-dNOPAUSE','-dQUIET','-dBATCH','-dSAFER',"-sOutputFile=$saida",$f.FullName) + $tarefas += @{ Exe=$gsExe; Rotulo=$f.Name; Alvo=$saida; Args=$a } +} +if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow } + +Titulo 'Comprimindo' +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao 'imagem' + Write-Host ("{0} PDF(s), até {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + Relatar $rs $tarefas ([ref]$ok) ([ref]$err) + Write-Host '' + $totA = 0; $totD = 0 + foreach ($t in $tarefas) { + if (Test-Path -LiteralPath $t.Alvo) { + $a = $antes[$t.Alvo]; $d = (Get-Item -LiteralPath $t.Alvo).Length + $totA += $a; $totD += $d + $pc = if ($a -gt 0) { [int]((1 - ($d / $a)) * 100) } else { 0 } + Write-Host (" {0,-42} {1,10} -> {2,10} ({3}% menor)" -f $t.Rotulo, (Tam $a), (Tam $d), $pc) -ForegroundColor Gray + } + } + if ($totA -gt 0) { + Write-Host '' + Write-Host ("TOTAL: {0} -> {1} ({2}% menor)" -f (Tam $totA), (Tam $totD), [int]((1 - ($totD/$totA)) * 100)) -ForegroundColor Cyan + } +} +Resumo $ok $ign $err $destino
@@ -12,6 +12,155 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = 'Converter Imagens' } catch {} +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + function Pausar { Write-Host '' Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray@@ -278,31 +427,21 @@
$convertidos = 0 $ignorados = 0 $erros = 0 -$indice = 0 +# --- 1) monta as tarefas (barato, sem processar nada ainda) --- +$tarefas = @() 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) { $ignorados++; continue } - 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 + # GIF/multiframe indo para formato de imagem unica: pega o 1o quadro $entradaMagick = $arquivo.FullName if ($arquivo.Extension.ToLowerInvariant() -eq '.gif' -and $extSaida -ne '.gif') { $entradaMagick = "$($arquivo.FullName)[0]"@@ -310,27 +449,36 @@ }
$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') - } + if ($extSaida -in '.jpg', '.jpeg', '.webp') { $imgArgs += @('-quality', '85') } $imgArgs += $saida - & magick @imgArgs + $tarefas += @{ Exe = 'magick'; Rotulo = $arquivo.Name; Alvo = $saida; Args = $imgArgs } +} + +if ($ignorados -gt 0) { Write-Host "$ignorados ja existiam no destino e foram puladas." -ForegroundColor Yellow } - 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++ +# --- 2) processa em paralelo --- +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao 'imagem' + Write-Host ("Convertendo {0} imagem(ns), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + + # --- 3) resumo, mostrando o motivo real de cada falha --- + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $convertidos++ } + else { + $erros++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } } } - # ------------------------------------------------------------ # 6) Resumo final # ------------------------------------------------------------
@@ -12,6 +12,155 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = 'Imagens e PDF' } catch {} +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } function Titulo($t) { Write-Host ''@@ -116,19 +265,33 @@ Salvar-Config ([pscustomobject]@{ UltimaSaidaPdf=($config.UltimaSaidaPdf); UltimaSaidaImg=$destino })
Titulo 'Processando' $ok=0;$err=0 +$tarefas = @(); $pastas = @() foreach ($pf in $pdfs) { $outDir = Join-Path $destino ($pf.BaseName + '-imagens') New-Item -ItemType Directory -Path $outDir -Force | Out-Null $padraoSaida = Join-Path $outDir ('pagina-%03d' + $extSaida) - Write-Host ''; Write-Host $pf.Name -ForegroundColor Magenta - $imgArgs = @('-density',"$dpi",$pf.FullName) - if ($extSaida -eq '.jpg') { $imgArgs += @('-quality','90','-background','white','-alpha','remove') } - $imgArgs += $padraoSaida - & magick @imgArgs - if ($LASTEXITCODE -eq 0) { - $qtd = @(Get-ChildItem -LiteralPath $outDir -File).Count - Write-Host " [OK] $qtd página(s) -> $outDir" -ForegroundColor Green; $ok++ - } else { Write-Host ' [ERRO] Falha ao converter.' -ForegroundColor Red; $err++ } + $a = @('-density',"$dpi",$pf.FullName) + if ($extSaida -eq '.jpg') { $a += @('-quality','90','-background','white','-alpha','remove') } + $a += $padraoSaida + $tarefas += @{ Exe='magick'; Rotulo=$pf.Name; Alvo=$outDir; Args=$a } + $pastas += $outDir +} +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao 'imagem' + Write-Host ("{0} PDF(s), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + for ($i = 0; $i -lt $tarefas.Count; $i++) { + $qtd = @(Get-ChildItem -LiteralPath $pastas[$i] -File -ErrorAction SilentlyContinue).Count + if ($rs[$i].Codigo -eq 0 -and $qtd -gt 0) { + Write-Host (" [OK] {0}: {1} pagina(s)" -f $tarefas[$i].Rotulo, $qtd) -ForegroundColor Green + $ok++ + } else { + $err++ + Write-Host (" [ERRO] {0}" -f $tarefas[$i].Rotulo) -ForegroundColor Red + $motivo = ($rs[$i].Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + } + } } Titulo 'Concluído' Write-Host "PDFs convertidos: $ok" -ForegroundColor Green
@@ -12,6 +12,155 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = "Marca d'água" } catch {} +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } function Titulo($t) { Write-Host ''@@ -114,30 +263,61 @@ Write-Host "Imagens encontradas: $($arquivos.Count)" -ForegroundColor Cyan
if ($arquivos.Count -eq 0) { Write-Host 'Nenhuma imagem encontrada.' -ForegroundColor Yellow; Pausar; exit 0 } New-Item -ItemType Directory -Path $destino -Force | Out-Null -$ok=0;$ign=0;$err=0;$i=0 +$ok=0;$ign=0;$err=0 + +# --- 1) mede as larguras: 1 processo por imagem, porem todos em paralelo --- +$sonda = @() +foreach ($a in $arquivos) { $sonda += @{ Exe='magick'; Rotulo=$a.Name; Args=@('identify','-format','%w',"$($a.FullName)[0]") } } +$larguras = @{} +if ($sonda.Count -gt 0) { + Write-Host 'Medindo as imagens...' -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $sonda -Limite (Limite-Padrao 'imagem') -SemProgresso + for ($i = 0; $i -lt $rs.Count; $i++) { + $w = 0 + if ($rs[$i] -and $rs[$i].Codigo -eq 0) { $w = ("$($rs[$i].Saida)".Trim() -replace '\D','') -as [int] } + if (-not $w -or $w -le 0) { $w = 1000 } + $larguras[$arquivos[$i].FullName] = [int]$w + } +} + +# --- 2) monta as tarefas de composicao --- +$tarefas = @() foreach ($a in $arquivos) { - $i++ $rel = $a.FullName.Substring($origem.Length).TrimStart('\'); $sub = Split-Path $rel -Parent $pastaSaida = if ([string]::IsNullOrWhiteSpace($sub)) { $destino } else { Join-Path $destino $sub } New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null $saida = Join-Path $pastaSaida $a.Name - Write-Host ''; Write-Host ("[{0}/{1}] {2}" -f $i,$arquivos.Count,$a.Name) -ForegroundColor Magenta - if (Test-Path -LiteralPath $saida) { Write-Host ' [IGNORADO] Já existe.' -ForegroundColor Yellow; $ign++; continue } + if (Test-Path -LiteralPath $saida) { $ign++; continue } + $w = [int]$larguras[$a.FullName]; if ($w -le 0) { $w = 1000 } if ($tipo -eq 'texto') { - $w = [int](& magick identify -format '%w' "$($a.FullName)[0]" 2>$null) - if (-not $w -or $w -le 0) { $w = 1000 } $ps = [int]($w / 22); if ($ps -lt 12) { $ps = 12 } $imgArgs = @($a.FullName,'-gravity',$grav,'-pointsize',"$ps",'-fill','rgba(255,255,255,0.85)','-undercolor','rgba(0,0,0,0.35)','-annotate',$offset,$texto,$saida) } else { - $w = [int](& magick identify -format '%w' "$($a.FullName)[0]" 2>$null) - if (-not $w -or $w -le 0) { $w = 1000 } $lw = [int]($w * $pct / 100); if ($lw -lt 10) { $lw = 10 } $imgArgs = @($a.FullName,'(',$logo,'-resize',"${lw}x",')','-gravity',$grav,'-geometry',$offset,'-composite',$saida) } - & magick @imgArgs - if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) { Write-Host ' [OK] Marca aplicada.' -ForegroundColor Green; $ok++ } - else { Write-Host ' [ERRO] Falha.' -ForegroundColor Red; if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }; $err++ } + $tarefas += @{ Exe='magick'; Rotulo=$a.Name; Alvo=$saida; Args=$imgArgs } +} + +if ($ign -gt 0) { Write-Host "$ign ja existiam no destino e foram puladas." -ForegroundColor Yellow } + +# --- 3) aplica em paralelo --- +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao 'imagem' + Write-Host ("Aplicando em {0} imagem(ns), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok++ } + else { + $err++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } } Titulo 'Concluído' Write-Host "Aplicadas: $ok" -ForegroundColor Green
@@ -36,15 +36,47 @@ Write-Host 'Analisando... (agrupa por tamanho e depois compara o conteúdo)' -ForegroundColor Gray
$todos = @(Get-ChildItem -LiteralPath $pasta -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Length -gt 0 }) # 1) agrupa por tamanho (rápido); 2) só calcula hash de quem tem tamanho repetido -$porTamanho = $todos | Group-Object Length | Where-Object { $_.Count -gt 1 } +function Hash-SHA256([string]$caminho) { + $sha = [System.Security.Cryptography.SHA256]::Create() + $fs = [System.IO.File]::OpenRead($caminho) + try { return ([BitConverter]::ToString($sha.ComputeHash($fs)) -replace '-', '') } + finally { $fs.Close(); $sha.Dispose() } +} +# Pre-filtro barato: hash so das PONTAS do arquivo (primeiros/ultimos 64 KB). +# So quem colide nas pontas paga o SHA256 do arquivo inteiro. +function Hash-Parcial([string]$caminho, [long]$tam) { + $n = 65536 + try { $fs = [System.IO.File]::OpenRead($caminho) } catch { return "ERRO$caminho" } + try { + $md5 = [System.Security.Cryptography.MD5]::Create() + $buf = New-Object byte[] $n + $lido = $fs.Read($buf, 0, $n) + [void]$md5.TransformBlock($buf, 0, $lido, $buf, 0) + if ($tam -gt ($n * 2)) { + [void]$fs.Seek(-$n, [System.IO.SeekOrigin]::End) + $lido2 = $fs.Read($buf, 0, $n) + [void]$md5.TransformBlock($buf, 0, $lido2, $buf, 0) + } + [void]$md5.TransformFinalBlock((New-Object byte[] 0), 0, 0) + return ([BitConverter]::ToString($md5.Hash) + "_$tam") + } catch { return "ERRO$caminho" } finally { $fs.Close() } +} + +$porTamanho = @($todos | Group-Object Length | Where-Object { $_.Count -gt 1 }) $grupos = @() +$gi = 0 foreach ($g in $porTamanho) { - $porHash = $g.Group | Group-Object { (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash } - foreach ($h in ($porHash | Where-Object { $_.Count -gt 1 })) { - $grupos += ,@($h.Group | Sort-Object FullName) + $gi++ + Write-Host ("`r Comparando... {0}/{1} grupos " -f $gi, $porTamanho.Count) -NoNewline -ForegroundColor Cyan + $porParcial = $g.Group | Group-Object { Hash-Parcial $_.FullName $_.Length } + foreach ($pp in ($porParcial | Where-Object { $_.Count -gt 1 })) { + $porHash = $pp.Group | Group-Object { Hash-SHA256 $_.FullName } + foreach ($h in ($porHash | Where-Object { $_.Count -gt 1 })) { + $grupos += ,@($h.Group | Sort-Object FullName) + } } } - +if ($porTamanho.Count -gt 0) { Write-Host '' } if ($grupos.Count -eq 0) { Titulo 'Resultado'; Write-Host 'Nenhum arquivo duplicado encontrado.' -ForegroundColor Green; Pausar; exit 0 } $totalDup = 0; $espaco = 0
@@ -0,0 +1,193 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Compactar / Extrair - compacta e extrai arquivos em lote +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Compactar / Extrair' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'CompactarZip' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Compactar / Extrair' +# Get-Command devolve .Source e Get-Item devolve .FullName: normaliza tudo +# num caminho unico para nao misturar os dois tipos. +$seteExe = $null +$c7 = Get-Command 7z -ErrorAction SilentlyContinue +if ($c7) { $seteExe = $c7.Source } +else { + foreach ($cand in @("$env:ProgramFiles\7-Zip\7z.exe", "${env:ProgramFiles(x86)}\7-Zip\7z.exe")) { + if (Test-Path -LiteralPath $cand) { $seteExe = $cand; break } + } +} +if ($seteExe) { Write-Host ("[OK] 7-Zip: {0}" -f $seteExe) -ForegroundColor Green } +else { Write-Host '[OK] Usando o compactador do Windows (instale o 7-Zip para mais formatos: winget install 7zip.7zip)' -ForegroundColor Gray } +$config = Carregar-Config + +Titulo 'O que você quer fazer?' +Write-Host ' [1] Compactar uma pasta' -ForegroundColor White +Write-Host ' [2] Extrair arquivos compactados' -ForegroundColor White +$acao = $null +while (-not $acao) { $a = Read-Host 'Opção (1-2)'; switch ($a.Trim()) { '1' { $acao='zip' } '2' { $acao='unzip' } default { Write-Host '[!] Digite 1 ou 2.' -ForegroundColor Yellow } } } + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } + +if ($acao -eq 'zip') { + $pasta = $null + while (-not $pasta) { + Titulo 'Pasta para compactar' + Write-Host " $padrao" -ForegroundColor White + $r = Read-Host 'Pasta (Enter = confirmar)' + $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x } + } + $nome = (Read-Host "Nome do arquivo (Enter = $([System.IO.Path]::GetFileName($pasta)).zip)").Trim() + if ([string]::IsNullOrWhiteSpace($nome)) { $nome = [System.IO.Path]::GetFileName($pasta) + '.zip' } + if ($nome -notmatch '\.(zip|7z)$') { $nome += '.zip' } + $destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Split-Path $pasta -Parent }) + Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta; UltimaSaida=$destino }) + $saida = Join-Path $destino $nome + if (Test-Path -LiteralPath $saida) { + $s = Read-Host 'Já existe um arquivo com esse nome. Sobrescrever? (s/N)' + if ($s -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 } + Remove-Item -LiteralPath $saida -Force + } + Titulo 'Compactando' + Write-Host "Saída: $saida" + $t = Measure-Command { + if ($seteExe) { & $seteExe a -mmt=on -- $saida (Join-Path $pasta '*') | Out-Null; $global:rc = $LASTEXITCODE } + else { Compress-Archive -Path (Join-Path $pasta '*') -DestinationPath $saida -CompressionLevel Optimal -Force; $global:rc = 0 } + } + if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) { + Write-Host ("[OK] Pronto em {0:N1}s ({1})" -f $t.TotalSeconds, (Tam (Get-Item $saida).Length)) -ForegroundColor Green + Resumo 1 0 0 $destino + } else { Write-Host '[ERRO] Falha ao compactar.' -ForegroundColor Red; Resumo 0 0 1 $destino } +} +else { + $ent = Pedir-Entrada $padrao @('.zip','.7z','.rar','.tar','.gz') 'Pasta ou arquivo compactado' + $itens = Listar $ent @('.zip','.7z','.rar','.tar','.gz') + if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo compactado encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } + $destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'Extraido' }) + Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimaSaida=$destino }) + Titulo 'Extraindo' + $ok=0;$err=0 + foreach ($f in $itens) { + $sub = Join-Path $destino $f.BaseName + New-Item -ItemType Directory -Path $sub -Force | Out-Null + Write-Host (" {0}..." -f $f.Name) -ForegroundColor Gray + $falhou = $false + if ($seteExe) { & $seteExe x -y "-o$sub" -- $f.FullName | Out-Null; if ($LASTEXITCODE -ne 0) { $falhou = $true } } + elseif ($f.Extension -eq '.zip') { try { Expand-Archive -LiteralPath $f.FullName -DestinationPath $sub -Force } catch { $falhou = $true } } + else { Write-Host ' (formato exige o 7-Zip)' -ForegroundColor Yellow; $falhou = $true } + if ($falhou) { $err++ } else { $ok++ } + } + Resumo $ok 0 $err $destino +}
@@ -0,0 +1,321 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Limpar Metadados - remove EXIF/GPS de fotos e vídeos (privacidade) +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Limpar Metadados' } catch {} + +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'LimparMetadados' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Limpar Metadados' +Checar 'magick' 'ImageMagick' 'winget install ImageMagick.ImageMagick' | Out-Null +$temFfmpeg = [bool](Get-Command ffmpeg -ErrorAction SilentlyContinue) +if ($temFfmpeg) { Write-Host '[OK] FFmpeg encontrado (vídeos também serão limpos).' -ForegroundColor Green } +else { Write-Host '[!] Sem FFmpeg: só as imagens serão limpas.' -ForegroundColor Yellow } +$config = Carregar-Config + +Write-Host '' +Write-Host 'Remove EXIF, GPS, câmera, data e outros metadados - útil antes de publicar.' -ForegroundColor DarkGray +Write-Host 'Os arquivos ORIGINAIS não são alterados: as cópias limpas vão para a pasta de saída.' -ForegroundColor DarkGray + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$aceitos = $extImg + $extVideo +$ent = Pedir-Entrada $padrao $aceitos 'Pasta ou arquivo' +$itens = Listar $ent $aceitos +if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } + +# previa: mostra o que existe hoje numa amostra +Titulo 'Prévia' +$amostra = $itens | Where-Object { $extImg -contains $_.Extension.ToLowerInvariant() } | Select-Object -First 1 +if ($amostra) { + $info = & magick identify -format '%[EXIF:Make] %[EXIF:Model] %[EXIF:DateTimeOriginal] %[EXIF:GPSLatitude]' "$($amostra.FullName)[0]" 2>$null + if ("$info".Trim()) { Write-Host (" Exemplo ({0}) contém: {1}" -f $amostra.Name, "$info".Trim()) -ForegroundColor Yellow } + else { Write-Host (" Exemplo ({0}): sem EXIF relevante." -f $amostra.Name) -ForegroundColor Gray } +} +Write-Host (" {0} arquivo(s) serão limpos." -f $itens.Count) -ForegroundColor White + +$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'SemMetadados' }) +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimaSaida=$destino }) + +$ok=0;$ign=0;$err=0 +$tarefas = @() +foreach ($f in $itens) { + $saida = Join-Path $destino $f.Name + if (Test-Path -LiteralPath $saida) { $ign++; continue } + if ($extImg -contains $f.Extension.ToLowerInvariant()) { + $tarefas += @{ Exe='magick'; Rotulo=$f.Name; Alvo=$saida; Args=@($f.FullName,'-strip',$saida) } + } elseif ($temFfmpeg) { + $tarefas += @{ Exe='ffmpeg'; Rotulo=$f.Name; Alvo=$saida + Args=@('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName,'-map_metadata','-1','-c','copy','-y',$saida) } + } +} +if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow } + +Titulo 'Limpando' +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao 'imagem' + Write-Host ("{0} arquivo(s), até {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + Relatar $rs $tarefas ([ref]$ok) ([ref]$err) +} +Resumo $ok $ign $err $destino
@@ -0,0 +1,185 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Limpar Nomes - normaliza nomes de arquivo para web/upload +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Limpar Nomes' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'LimparNomes' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Limpar Nomes' +Write-Host '[OK] Não precisa de nenhum programa extra.' -ForegroundColor Green +$config = Carregar-Config + +Write-Host '' +Write-Host 'Deixa os nomes seguros para web/upload: tira acentos, troca espaços por hífen e usa minúsculas.' -ForegroundColor DarkGray + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$pasta = $null +while (-not $pasta) { + Titulo 'Pasta' + Write-Host " $padrao" -ForegroundColor White + $r = Read-Host 'Pasta (Enter = confirmar)' + $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x } +} +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta }) + +function Limpar-Nome([string]$nome) { + $base = [System.IO.Path]::GetFileNameWithoutExtension($nome) + $ext = [System.IO.Path]::GetExtension($nome).ToLowerInvariant() + $n = $base.Normalize([System.Text.NormalizationForm]::FormD) + $sb = New-Object System.Text.StringBuilder + foreach ($ch in $n.ToCharArray()) { + if ([System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($ch) -ne [System.Globalization.UnicodeCategory]::NonSpacingMark) { [void]$sb.Append($ch) } + } + $r = $sb.ToString().ToLowerInvariant() + $r = $r -replace '[^a-z0-9]+', '-' + $r = $r -replace '-+', '-' + $r = $r.Trim('-') + if ([string]::IsNullOrWhiteSpace($r)) { $r = 'arquivo' } + return $r + $ext +} + +$arquivos = @(Get-ChildItem -LiteralPath $pasta -File | Sort-Object Name) +if ($arquivos.Count -eq 0) { Write-Host 'Nenhum arquivo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 } + +$plano = @(); $reservados = @() +foreach ($a in $arquivos) { + $novo = Limpar-Nome $a.Name + if ($novo -ceq $a.Name) { continue } + $alvo = Join-Path $pasta $novo + $b = [System.IO.Path]::GetFileNameWithoutExtension($novo); $e = [System.IO.Path]::GetExtension($novo); $i = 1 + while ((Test-Path -LiteralPath $alvo) -or ($reservados -contains $alvo)) { $alvo = Join-Path $pasta ("$b-$i$e"); $i++ } + $reservados += $alvo + $plano += [pscustomobject]@{ De=$a; Para=$alvo } +} +if ($plano.Count -eq 0) { Write-Host 'Todos os nomes já estão limpos. Nada a fazer.' -ForegroundColor Green; Pausar; exit 0 } + +Titulo 'Prévia' +$mostra = [Math]::Min($plano.Count, 15) +for ($i=0; $i -lt $mostra; $i++) { + Write-Host (" {0}" -f $plano[$i].De.Name) -ForegroundColor Gray -NoNewline + Write-Host (" -> {0}" -f (Split-Path $plano[$i].Para -Leaf)) -ForegroundColor White +} +if ($plano.Count -gt $mostra) { Write-Host (" ... e mais {0}." -f ($plano.Count - $mostra)) -ForegroundColor DarkGray } +Write-Host '' +$ok = Read-Host "Renomear $($plano.Count) arquivo(s)? (s/N)" +if ($ok -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 } + +$feitos=0;$err=0 +foreach ($p in $plano) { + try { Rename-Item -LiteralPath $p.De.FullName -NewName (Split-Path $p.Para -Leaf) -ErrorAction Stop; $feitos++ } + catch { Write-Host (" [ERRO] {0}: {1}" -f $p.De.Name, $_.Exception.Message) -ForegroundColor Red; $err++ } +} +Resumo $feitos 0 $err $pasta
@@ -13,6 +13,155 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = 'Renomear em lote' } catch {} +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } function Titulo($t) { Write-Host ''@@ -32,7 +181,8 @@ $extImgExif = @('.jpg','.jpeg','.tif','.tiff','.heic','.heif','.png','.webp')
function Data-Do-Arquivo($file) { if ($magick -and ($extImgExif -contains $file.Extension.ToLowerInvariant())) { try { - $d = & magick identify -format '%[EXIF:DateTimeOriginal]' "$($file.FullName)[0]" 2>$null + $d = if ($cacheExif -and $cacheExif.ContainsKey($file.FullName)) { $cacheExif[$file.FullName] } + else { & magick identify -format '%[EXIF:DateTimeOriginal]' "$($file.FullName)[0]" 2>$null } if ($d -and ($d.Trim() -match '^\d{4}:\d{2}:\d{2}\s\d{2}:\d{2}:\d{2}$')) { return [datetime]::ParseExact($d.Trim(),'yyyy:MM:dd HH:mm:ss',$null) }@@ -51,6 +201,22 @@ $p = (Read-Host 'Cole o caminho da pasta').Trim().Trim('"')
if (Test-Path -LiteralPath $p -PathType Container) { $pasta = (Resolve-Path -LiteralPath $p).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow } } $arquivos = @(Get-ChildItem -LiteralPath $pasta -File | Sort-Object Name) + +# Le as datas EXIF de TODAS as fotos de uma vez (antes era 1 processo por +# arquivo, em serie). O resultado alimenta Data-Do-Arquivo pelo cache. +$cacheExif = @{} +if ($magick) { + $fotos = @($arquivos | Where-Object { $extImgExif -contains $_.Extension.ToLowerInvariant() }) + if ($fotos.Count -gt 1) { + Write-Host 'Lendo as datas das fotos...' -ForegroundColor Gray + $tar = @() + foreach ($f in $fotos) { $tar += @{ Exe='magick'; Rotulo=$f.Name; Args=@('identify','-format','%[EXIF:DateTimeOriginal]',"$($f.FullName)[0]") } } + $rs = Executar-EmParalelo -Tarefas $tar -Limite (Limite-Padrao 'imagem') -SemProgresso + for ($i = 0; $i -lt $fotos.Count; $i++) { + if ($rs[$i] -and $rs[$i].Codigo -eq 0) { $cacheExif[$fotos[$i].FullName] = "$($rs[$i].Saida)".Trim() } + } + } +} if ($arquivos.Count -eq 0) { Write-Host 'Nenhum arquivo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 } Titulo 'Modo de renomeação'
@@ -1,11 +1,95 @@
# fast-scripts Programas rápidos para Windows, em lote e com interface no terminal. +**26 programas** que você abre com um duplo-clique — sem instalar nada, sem linha de comando.    +## Começando + +Abra o **`canivete.cmd`** na raiz: ele lista todos os programas e abre o que você escolher. +Ou vá direto na pasta e dê um duplo-clique no programa que quiser. + +Todos seguem a mesma lógica: perguntam a **pasta ou o arquivo**, as **opções**, e a **pasta de saída** — lembrando suas escolhas da próxima vez e criando a pasta se ela não existir. Nada destrutivo acontece sem prévia e confirmação. + +## Os programas + +### 🎬 Vídeos + +| Programa | O que faz | +|---|---| +| [transcodificar](docs/transcodificar.md) | Converte vídeos em lote, com GPU e 5 níveis de qualidade | +| [baixar-videos](docs/baixar-videos.md) | Baixa vídeos e áudio de mais de mil sites (yt-dlp) | +| [extrair-audio](docs/extrair-audio.md) | Tira o áudio de vídeos (MP3/M4A/WAV) | +| [cortar-video](docs/cortar-video.md) | Corta um trecho do vídeo — instantâneo | +| [juntar-videos](docs/juntar-videos.md) | Junta vários vídeos num só | +| [criar-gif](docs/criar-gif.md) | Vídeo vira GIF, ou fotos viram slideshow | +| [marca-dagua-video](docs/marca-dagua-video.md) | Marca d'água de texto ou logo em vídeos | +| [normalizar-audio](docs/normalizar-audio.md) | Padroniza o volume (EBU R128) | +| [info-midia](docs/info-midia.md) | Relatório de codec, resolução e bitrate + planilha | + +### 🖼️ Imagens + +| Programa | O que faz | +|---|---| +| [converter-imagens](docs/converter-imagens.md) | Redimensiona e converte imagens em lote | +| [marca-dagua](docs/marca-dagua.md) | Marca d'água de texto ou logo em fotos | +| [imagens-e-pdf](docs/imagens-e-pdf.md) | Junta imagens num PDF ou explode PDF em imagens | +| [comprimir-pdf](docs/comprimir-pdf.md) | Reduz o tamanho de PDFs | + +### 🗂️ Organizar + +| Programa | O que faz | +|---|---| +| [renomear-em-lote](docs/renomear-em-lote.md) | Renomeia por sequência, data ou localizar/substituir | +| [achar-duplicados](docs/achar-duplicados.md) | Acha arquivos iguais por conteúdo e libera espaço | +| [organizar-por-tipo](docs/organizar-por-tipo.md) | Separa a bagunça em subpastas por categoria | +| [compactar-zip](docs/compactar-zip.md) | Compacta e extrai arquivos | +| [limpar-metadados](docs/limpar-metadados.md) | Remove EXIF e GPS de fotos e vídeos | +| [limpar-nomes](docs/limpar-nomes.md) | Deixa os nomes de arquivo seguros para web | + +### 🧹 Úteis + +| Programa | O que faz | +|---|---| +| [backup-espelhado](docs/backup-espelhado.md) | Backup incremental e rápido com robocopy | +| [verificar-discos](docs/verificar-discos.md) | Saúde dos HDs e SSDs (SMART) | +| [verificar-integridade](docs/verificar-integridade.md) | Detecta arquivos corrompidos silenciosamente | +| [analisar-espaco](docs/analisar-espaco.md) | Descobre o que está enchendo o disco | +| [limpeza-segura](docs/limpeza-segura.md) | Limpa temporários e lixeira, com prévia | +| [relatorio-sistema](docs/relatorio-sistema.md) | Specs do PC, memória, discos e espaço | + +### 🔪 Raiz + +| Programa | O que faz | +|---|---| +| [canivete](docs/canivete.md) | Menu central que abre todos os outros | + +## Instalação + +Não tem instalação: é só copiar a pasta. O que alguns programas precisam: + +``` +winget install Gyan.FFmpeg # vídeo e áudio +winget install ImageMagick.ImageMagick # imagens +winget install ArtifexSoftware.GhostScript # PDF +winget install 7zip.7zip # opcional: .rar e .7z +winget install smartmontools # opcional: SMART avançado +``` + +O **yt-dlp se instala sozinho** quando necessário. Cada programa avisa na tela se faltar algo — veja a lista completa em **[docs/DEPENDENCIAS.md](docs/DEPENDENCIAS.md)**. + +## Velocidade + +Os programas que trabalham em lote processam **vários arquivos ao mesmo tempo**, ajustando o número de processos ao tipo de trabalho. Em imagens o ganho medido chega a **4,2×**. O `transcodificar` e o `marca-dagua-video` usam a **placa NVIDIA** quando ela existe, e o `extrair-audio` **copia o áudio sem recodificar** quando o formato já é o certo — nesse caso é quase instantâneo. + +## Para quem for mexer no código + +As regras técnicas da coleção (cabeçalho polyglot, codificação, paralelismo e as armadilhas já mapeadas) estão em **[docs/PADROES.md](docs/PADROES.md)**. + --- **Pablo Murad** — pablomurad[at]pm[dot]me +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,192 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Baixar Vídeos - download de vídeos/áudio (yt-dlp) +# Baixa de YouTube, Instagram, TikTok, X, Vimeo e +1000 sites +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Baixar Vídeos' } catch {} + +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} + +$configDir = Join-Path $env:APPDATA 'BaixarVideos' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Perfis de qualidade / formato +$perfis = @( + [pscustomobject]@{ Id=1; Nome='Melhor qualidade (resolução máxima)'; Tipo='video'; Altura=0 }, + [pscustomobject]@{ Id=2; Nome='1080p (Full HD)'; Tipo='video'; Altura=1080 }, + [pscustomobject]@{ Id=3; Nome='720p (HD)'; Tipo='video'; Altura=720 }, + [pscustomobject]@{ Id=4; Nome='480p'; Tipo='video'; Altura=480 }, + [pscustomobject]@{ Id=5; Nome='360p (compacto)'; Tipo='video'; Altura=360 }, + [pscustomobject]@{ Id=6; Nome='Só áudio - MP3'; Tipo='mp3'; Altura=0 }, + [pscustomobject]@{ Id=7; Nome='Só áudio - M4A'; Tipo='m4a'; Altura=0 } +) + +Titulo 'Baixar Vídeos' +Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray +Write-Host '' +Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray + +# ------------------------------------------------------------ +# 1) Localizar / baixar o yt-dlp (o baixador mais moderno) +# ------------------------------------------------------------ +$localDir = Join-Path $env:LOCALAPPDATA 'fast-scripts' +$localExe = Join-Path $localDir 'yt-dlp.exe' +$ytdlp = $null +$cmd = Get-Command yt-dlp -ErrorAction SilentlyContinue +if ($cmd) { $ytdlp = $cmd.Source } +elseif (Test-Path -LiteralPath $localExe) { $ytdlp = $localExe } +else { + Write-Host '' + Write-Host 'yt-dlp não encontrado. Baixando a versão mais recente (site oficial)...' -ForegroundColor Yellow + try { + New-Item -ItemType Directory -Path $localDir -Force | Out-Null + try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + $url = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe' + Invoke-WebRequest -Uri $url -OutFile $localExe -UseBasicParsing + $ytdlp = $localExe + Write-Host '[OK] yt-dlp baixado.' -ForegroundColor Green + } catch { + Write-Host "[ERRO] Não foi possível baixar o yt-dlp: $($_.Exception.Message)" -ForegroundColor Red + Write-Host 'Verifique sua internet e tente de novo.' -ForegroundColor Yellow + Pausar; exit 1 + } +} +Write-Host "[OK] yt-dlp: $ytdlp" -ForegroundColor Green + +# Auto-atualiza SÓ a cópia que este programa baixou (não mexe numa instalação sua) +if ($ytdlp -eq $localExe) { + $idadeDias = (New-TimeSpan -Start (Get-Item $localExe).LastWriteTime -End (Get-Date)).TotalDays + if ($idadeDias -gt 15) { + Write-Host 'Procurando atualização do yt-dlp...' -ForegroundColor Gray + & $ytdlp -U 2>$null | Out-Null + } +} + +# ffmpeg (necessário p/ juntar vídeo+áudio de alta qualidade e converter áudio) +$ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue +$ffmpegDir = if ($ffmpeg) { Split-Path $ffmpeg.Source -Parent } else { $null } +if ($ffmpeg) { Write-Host "[OK] FFmpeg: $($ffmpeg.Source)" -ForegroundColor Green } +else { Write-Host '[!] FFmpeg não encontrado - alta qualidade e conversão de áudio podem falhar. Instale: winget install Gyan.FFmpeg' -ForegroundColor Yellow } + +$config = Carregar-Config + +# ------------------------------------------------------------ +# 2) Link(s) +# ------------------------------------------------------------ +$urls = @() +while ($urls.Count -eq 0) { + Titulo 'Link do vídeo' + Write-Host 'Cole o link (ou vários separados por espaço).' -ForegroundColor DarkGray + Write-Host 'Funciona com YouTube, Instagram, TikTok, X, Vimeo, Facebook e +1000 sites.' -ForegroundColor DarkGray + $resp = Read-Host 'Link' + $urls = @($resp -split '\s+' | Where-Object { $_ -match '^https?://' }) + if ($urls.Count -eq 0) { Write-Host '[!] Cole um link válido (começando com http).' -ForegroundColor Yellow } +} + +# ------------------------------------------------------------ +# 3) Qualidade / formato +# ------------------------------------------------------------ +$qPadrao = if ($config -and $config.UltimaQualidade) { [int]$config.UltimaQualidade } else { 1 } +Titulo 'Qualidade / formato' +foreach ($p in $perfis) { + $m = if ($p.Id -eq $qPadrao) { '>' } else { ' ' } + $c = if ($p.Id -eq $qPadrao) { 'White' } else { 'Gray' } + Write-Host ("{0} [{1}] {2}" -f $m, $p.Id, $p.Nome) -ForegroundColor $c +} +Write-Host "Enter = usar a opção padrão [$qPadrao]" -ForegroundColor DarkGray +$perfil = $null +while (-not $perfil) { + $e = Read-Host 'Qualidade (1-7)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = "$qPadrao" } + $perfil = $perfis | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1 + if (-not $perfil) { Write-Host '[!] Escolha de 1 a 7.' -ForegroundColor Yellow } +} + +# ------------------------------------------------------------ +# 4) Playlist? +# ------------------------------------------------------------ +$plPadrao = if ($config -and $config.UltimaPlaylist) { "$($config.UltimaPlaylist)" } else { 'nao' } +Titulo 'Playlist' +Write-Host 'Se o link fizer parte de uma playlist, baixar a playlist INTEIRA?' -ForegroundColor Gray +$dicaPl = if ($plPadrao -eq 'sim') { '(Enter = SIM)' } else { '(Enter = NÃO)' } +$rpl = Read-Host "Baixar playlist inteira? (s/n) $dicaPl" +$playlist = if ([string]::IsNullOrWhiteSpace($rpl)) { $plPadrao -eq 'sim' } else { $rpl -match '^(s|sim|y|yes)$' } + +# ------------------------------------------------------------ +# 5) Pasta de saída (lembra a última; cria se não existir) +# ------------------------------------------------------------ +$saidaPadrao = if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $env:USERPROFILE 'Downloads\Baixados' } +while ($true) { + Titulo 'Pasta de saída' + Write-Host " $saidaPadrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $rsaida = Read-Host 'Pasta de saída' + $destino = if ([string]::IsNullOrWhiteSpace($rsaida)) { $saidaPadrao } else { $rsaida.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $destino -Force | Out-Null; break } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } +} +$destino = (Resolve-Path -LiteralPath $destino).Path + +Salvar-Config ([pscustomobject]@{ UltimaQualidade=$perfil.Id; UltimaPlaylist=$(if ($playlist) {'sim'} else {'nao'}); UltimaSaida=$destino }) + +# ------------------------------------------------------------ +# 6) Monta os argumentos do yt-dlp +# ------------------------------------------------------------ +switch ($perfil.Tipo) { + 'mp3' { $fmtArgs = @('-f','ba/b','-x','--audio-format','mp3','--audio-quality','0') } + 'm4a' { $fmtArgs = @('-f','ba/b','-x','--audio-format','m4a') } + default { + if ($perfil.Altura -eq 0) { $fmtArgs = @('-f','bv*+ba/b','--merge-output-format','mp4') } + else { $fmtArgs = @('-f',"bv*[height<=$($perfil.Altura)]+ba/b[height<=$($perfil.Altura)]/b[height<=$($perfil.Altura)]",'--merge-output-format','mp4') } + } +} + +$ytArgs = @() +$ytArgs += $fmtArgs +$ytArgs += @('-o', (Join-Path $destino '%(title)s.%(ext)s')) +$ytArgs += @('--embed-metadata','--embed-thumbnail') # título, capa, etc. +$ytArgs += @('--no-overwrites') # pula o que já foi baixado +$ytArgs += @('--concurrent-fragments','8') # baixa varios pedacos de uma vez +# O aria2c baixa em muitas conexoes ao mesmo tempo: bem mais rapido quando existe. +$aria = Get-Command aria2c -ErrorAction SilentlyContinue +if ($aria) { + Write-Host '[OK] aria2c encontrado: download acelerado ligado.' -ForegroundColor Green + $ytArgs += @('--downloader','aria2c','--downloader-args','aria2c:-x16 -s16 -k1M') +} # download mais rápido +$ytArgs += @('--ignore-errors') # em lote, continua se um falhar +$ytArgs += if ($playlist) { '--yes-playlist' } else { '--no-playlist' } +if ($ffmpegDir) { $ytArgs += @('--ffmpeg-location', $ffmpegDir) } + +# ------------------------------------------------------------ +# 7) Baixar +# ------------------------------------------------------------ +Titulo 'Baixando' +Write-Host "Qualidade: $($perfil.Nome)" +Write-Host "Playlist: $(if ($playlist) {'sim'} else {'não'})" +Write-Host "Destino: $destino" +Write-Host "Links: $($urls.Count)" +Write-Host '' + +& $ytdlp @ytArgs @urls +$rc = $LASTEXITCODE + +Titulo 'Concluído' +if ($rc -eq 0) { Write-Host '[OK] Download finalizado.' -ForegroundColor Green } +else { Write-Host "[!] Terminou com avisos/erros (código $rc). Alguns itens podem não ter baixado." -ForegroundColor Yellow } +Write-Host "Pasta: $destino" +Pausar
@@ -0,0 +1,188 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Cortar Vídeo - apara um trecho do vídeo (FFmpeg) +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Cortar Vídeo' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'CortarVideo' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Cortar Vídeo' +Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null +$config = Carregar-Config + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$ent = Pedir-Entrada $padrao $extVideo 'Vídeo para cortar' +if ($ent.Modo -ne 'arquivo') { + $lista = Listar $ent $extVideo + if ($lista.Count -eq 0) { Write-Host 'Nenhum vídeo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 } + Titulo 'Qual vídeo?' + for ($i=0; $i -lt $lista.Count; $i++) { Write-Host (" [{0}] {1}" -f ($i+1), $lista[$i].Name) -ForegroundColor Gray } + $sel = $null + while (-not $sel) { $e = Read-Host "Número (1-$($lista.Count))"; $n = $e -as [int]; if ($n -ge 1 -and $n -le $lista.Count) { $sel = $lista[$n-1] } else { Write-Host '[!] Inválido.' -ForegroundColor Yellow } } + $video = $sel +} else { $video = Get-Item -LiteralPath $ent.Caminho } + +# duração, para ajudar o usuário +$dur = 0.0 +try { + $b = (& ffprobe -v error -show_entries format=duration -of csv=p=0 $video.FullName 2>$null) + [double]::TryParse(("$b" -replace ',', '.'), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$dur) | Out-Null +} catch {} +Titulo 'Trecho' +Write-Host (" Vídeo: {0}" -f $video.Name) -ForegroundColor White +if ($dur -gt 0) { Write-Host (" Duração total: {0:hh\:mm\:ss}" -f [TimeSpan]::FromSeconds($dur)) -ForegroundColor Gray } +Write-Host ' Formato aceito: 90 ou 01:30 ou 00:01:30' -ForegroundColor DarkGray +$inicio = Read-Host 'Início (Enter = 0)' +if ([string]::IsNullOrWhiteSpace($inicio)) { $inicio = '0' } +$fim = Read-Host 'Fim (Enter = até o final)' + +Titulo 'Modo do corte' +Write-Host '> [1] Rápido - sem recodificar (instantâneo; corta no quadro-chave mais próximo)' -ForegroundColor White +Write-Host ' [2] Exato - recodifica (corta no ponto exato, demora mais)' -ForegroundColor Gray +$m = Read-Host 'Modo (Enter = 1)' +$exato = ($m.Trim() -eq '2') + +$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $video.DirectoryName 'Cortado' }) +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$video.DirectoryName; UltimaSaida=$destino }) + +$saida = Join-Path $destino ($video.BaseName + '-corte' + $video.Extension) +$n = 1 +while (Test-Path -LiteralPath $saida) { $saida = Join-Path $destino ($video.BaseName + "-corte ($n)" + $video.Extension); $n++ } + +$ffArgs = @('-hide_banner','-nostdin','-loglevel','error') +if ($exato) { + $ffArgs += @('-i', $video.FullName, '-ss', $inicio) + if (-not [string]::IsNullOrWhiteSpace($fim)) { $ffArgs += @('-to', $fim) } + $ffArgs += @('-c:v','libx264','-preset','veryfast','-crf','20','-c:a','aac','-b:a','160k') +} else { + $ffArgs += @('-ss', $inicio) + if (-not [string]::IsNullOrWhiteSpace($fim)) { $ffArgs += @('-to', $fim) } + $ffArgs += @('-i', $video.FullName, '-c', 'copy', '-avoid_negative_ts', 'make_zero') +} +$ffArgs += @('-y', $saida) + +Titulo 'Cortando' +Write-Host ("Modo: {0}" -f $(if ($exato) { 'exato (recodifica)' } else { 'rápido (sem recodificar)' })) +Write-Host "Saída: $saida" +Write-Host '' +$t = Measure-Command { & ffmpeg @ffArgs } +$rc = $LASTEXITCODE +if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) { + Write-Host ("[OK] Corte pronto em {0:N1}s ({1})" -f $t.TotalSeconds, (Tam (Get-Item $saida).Length)) -ForegroundColor Green + Resumo 1 0 0 $destino +} else { + Write-Host '[ERRO] Não foi possível cortar (confira os tempos informados).' -ForegroundColor Red + if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue } + Resumo 0 0 1 $destino +}
@@ -12,6 +12,155 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = 'Criar GIF / Slideshow' } catch {} +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } function Titulo($t) { Write-Host ''@@ -25,6 +174,19 @@ $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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} $extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.ts','.3gp','.vob') $extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')@@ -82,19 +244,54 @@ Write-Host "Vídeos: $($videos.Count)" -ForegroundColor Cyan
if ($videos.Count -eq 0) { Write-Host 'Nenhum vídeo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } New-Item -ItemType Directory -Path $destino -Force | Out-Null - $ok=0;$err=0;$i=0 + $ok=0;$ign=0;$err=0 + + # Cada GIF exige 2 passagens (gerar paleta + aplicar). Fazemos as duas + # fases em paralelo, em vez de 2 processos em serie por video. + $alvos = @() foreach ($v in $videos) { - $i++ $saida = Join-Path $destino ($v.BaseName + '.gif') - Write-Host ''; Write-Host ("[{0}/{1}] {2}" -f $i,$videos.Count,$v.Name) -ForegroundColor Magenta - if (Test-Path -LiteralPath $saida) { Write-Host ' [IGNORADO] Já existe.' -ForegroundColor Yellow; continue } - $paleta = Join-Path $env:TEMP ("paleta_{0}.png" -f ([guid]::NewGuid().ToString('N'))) - $filtro = "fps=$fps,scale=${larg}:-1:flags=lanczos" - & ffmpeg -hide_banner -nostdin -i $v.FullName -vf "$filtro,palettegen" -y $paleta 2>$null - & ffmpeg -hide_banner -nostdin -i $v.FullName -i $paleta -lavfi "$filtro [x]; [x][1:v] paletteuse" -y $saida 2>$null - Remove-Item -LiteralPath $paleta -Force -ErrorAction SilentlyContinue - if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) { Write-Host ' [OK] GIF criado.' -ForegroundColor Green; $ok++ } - else { Write-Host ' [ERRO] Falha.' -ForegroundColor Red; if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }; $err++ } + if (Test-Path -LiteralPath $saida) { $ign++; continue } + $alvos += [pscustomobject]@{ + Video = $v + Saida = $saida + Paleta = Join-Path $env:TEMP ("paleta_{0}.png" -f ([guid]::NewGuid().ToString('N'))) + Filtro = "fps=$fps,scale=${larg}:-1:flags=lanczos" + } + } + if ($ign -gt 0) { Write-Host "$ign ja existiam no destino e foram pulados." -ForegroundColor Yellow } + + if ($alvos.Count -gt 0) { + $lim = Limite-Padrao 'cpu' + Write-Host ("{0} video(s), ate {1} ao mesmo tempo..." -f $alvos.Count, $lim) -ForegroundColor Gray + + Write-Host ' Fase 1/2: gerando as paletas de cor...' -ForegroundColor DarkGray + $t1 = @() + foreach ($a in $alvos) { + $t1 += @{ Exe='ffmpeg'; Rotulo=$a.Video.Name; Alvo=$a.Paleta + Args=@('-hide_banner','-nostdin','-loglevel','error','-i',$a.Video.FullName,'-vf',"$($a.Filtro),palettegen",'-y',$a.Paleta) } + } + $r1 = Executar-EmParalelo -Tarefas $t1 -Limite $lim + + Write-Host ' Fase 2/2: montando os GIFs...' -ForegroundColor DarkGray + $t2 = @(); $mapa = @() + for ($i = 0; $i -lt $alvos.Count; $i++) { + if ($r1[$i].Codigo -ne 0 -or -not (Test-Path -LiteralPath $alvos[$i].Paleta)) { + $err++ + Write-Host (" [ERRO] {0} (paleta)" -f $alvos[$i].Video.Name) -ForegroundColor Red + continue + } + $a = $alvos[$i] + $t2 += @{ Exe='ffmpeg'; Rotulo=$a.Video.Name; Alvo=$a.Saida + Args=@('-hide_banner','-nostdin','-loglevel','error','-i',$a.Video.FullName,'-i',$a.Paleta, + '-lavfi',"$($a.Filtro) [x]; [x][1:v] paletteuse",'-y',$a.Saida) } + $mapa += $a + } + if ($t2.Count -gt 0) { + $r2 = Executar-EmParalelo -Tarefas $t2 -Limite $lim + Relatar $r2 $t2 ([ref]$ok) ([ref]$err) + } + foreach ($a in $alvos) { if (Test-Path -LiteralPath $a.Paleta) { Remove-Item -LiteralPath $a.Paleta -Force -ErrorAction SilentlyContinue } } } Titulo 'Concluído' Write-Host "GIFs criados: $ok" -ForegroundColor Green
@@ -12,6 +12,155 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = 'Extrair Áudio' } catch {} +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } function Titulo($t) { Write-Host ''@@ -106,19 +255,62 @@ Write-Host "Vídeos encontrados: $($arquivos.Count)" -ForegroundColor Cyan
if ($arquivos.Count -eq 0) { Write-Host 'Nenhum vídeo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } New-Item -ItemType Directory -Path $destino -Force | Out-Null -$ok=0;$ign=0;$err=0;$i=0 +# --- 1) sonda os codecs de audio em paralelo (para poder copiar sem recodificar) --- +$sonda = @() foreach ($a in $arquivos) { - $i++ + $sonda += @{ Exe = 'ffprobe'; Rotulo = $a.Name + Args = @('-v','error','-select_streams','a:0','-show_entries','stream=codec_name','-of','csv=p=0',$a.FullName) } +} +$codecs = @{} +if ($sonda.Count -gt 0) { + Write-Host 'Analisando os audios...' -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $sonda -Limite (Limite-Padrao 'sonda') -SemProgresso + for ($i = 0; $i -lt $rs.Count; $i++) { + $c = '' + if ($rs[$i] -and $rs[$i].Codigo -eq 0) { $c = ($rs[$i].Saida -split "`n" | Where-Object { $_.Trim() } | Select-Object -First 1) } + $codecs[$arquivos[$i].FullName] = "$c".Trim() + } +} + +$ok=0;$ign=0;$err=0;$copiados=0 + +# --- 2) monta as tarefas --- +$tarefas = @() +foreach ($a in $arquivos) { $rel = $a.FullName.Substring($origem.Length).TrimStart('\'); $sub = Split-Path $rel -Parent $pastaSaida = if ([string]::IsNullOrWhiteSpace($sub)) { $destino } else { Join-Path $destino $sub } New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null $saida = Join-Path $pastaSaida ($a.BaseName + $formato.Ext) - Write-Host ''; Write-Host ("[{0}/{1}] {2}" -f $i,$arquivos.Count,$a.Name) -ForegroundColor Magenta - if (Test-Path -LiteralPath $saida) { Write-Host ' [IGNORADO] Já existe.' -ForegroundColor Yellow; $ign++; continue } - $ffArgs = @('-hide_banner','-nostdin','-i',$a.FullName,'-vn') + $formato.Args + @('-y',$saida) - & ffmpeg @ffArgs - if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) { Write-Host ' [OK] Áudio extraído.' -ForegroundColor Green; $ok++ } - else { Write-Host ' [ERRO] Falha (o vídeo tem áudio?).' -ForegroundColor Red; if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }; $err++ } + if (Test-Path -LiteralPath $saida) { $ign++; continue } + + # Se o codec da fonte ja e' o do destino, copia o stream: quase instantaneo + $cod = "$($codecs[$a.FullName])".ToLowerInvariant() + $podeCopiar = ($formato.Ext -eq '.m4a' -and $cod -eq 'aac') -or ($formato.Ext -eq '.mp3' -and $cod -eq 'mp3') + if ($podeCopiar) { $audioArgs = @('-c:a','copy'); $copiados++ } else { $audioArgs = $formato.Args } + + $ffArgs = @('-hide_banner','-nostdin','-loglevel','error','-i',$a.FullName,'-vn') + $audioArgs + @('-y',$saida) + $tarefas += @{ Exe = 'ffmpeg'; Rotulo = $a.Name; Alvo = $saida; Args = $ffArgs } +} + +if ($ign -gt 0) { Write-Host "$ign ja existiam no destino e foram pulados." -ForegroundColor Yellow } +if ($copiados -gt 0) { Write-Host "$copiados serao copiados sem recodificar (bem mais rapido)." -ForegroundColor Green } + +# --- 3) executa em paralelo --- +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao 'cpu' + Write-Host ("Extraindo de {0} video(s), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok++ } + else { + $err++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } } Titulo 'Concluído' Write-Host "Extraídos: $ok" -ForegroundColor Green
@@ -0,0 +1,317 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Info de Mídia - relatório de codec, resolução e bitrate (FFprobe) +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Info de Mídia' } catch {} + +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'InfoMidia' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Info de Mídia' +Checar 'ffprobe' 'FFprobe (FFmpeg)' 'winget install Gyan.FFmpeg' | Out-Null +$config = Carregar-Config + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$ent = Pedir-Entrada $padrao ($extVideo + $extAudio) 'Pasta ou arquivo de mídia' +$itens = Listar $ent ($extVideo + $extAudio) +if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo de mídia encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } + +Titulo 'Analisando' +Write-Host ("{0} arquivo(s). Lendo em paralelo..." -f $itens.Count) -ForegroundColor Gray +$tarefas = @() +foreach ($f in $itens) { + $tarefas += @{ Exe='ffprobe'; Rotulo=$f.Name + Args=@('-v','error','-show_entries','format=duration,bit_rate:stream=codec_type,codec_name,width,height,channels','-of','default=noprint_wrappers=1',$f.FullName) } +} +$rs = Executar-EmParalelo -Tarefas $tarefas -Limite (Limite-Padrao 'sonda') + +$linhas = @() +for ($i=0; $i -lt $itens.Count; $i++) { + $f = $itens[$i]; $s = if ($rs[$i]) { "$($rs[$i].Saida)" } else { '' } + function Pega($chave) { $m = [regex]::Match($s, "(?m)^$chave=(.+)$"); if ($m.Success) { $m.Groups[1].Value.Trim() } else { '' } } + $dur = 0.0; [double]::TryParse(((Pega 'duration') -replace ',', '.'), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$dur) | Out-Null + $w = Pega 'width'; $h = Pega 'height' + $codecs = [regex]::Matches($s, "(?m)^codec_name=(.+)$") | ForEach-Object { $_.Groups[1].Value.Trim() } + $br = Pega 'bit_rate'; $brTxt = if ($br -as [double]) { "{0:N0} kbps" -f ([double]$br / 1000) } else { '' } + $linhas += [pscustomobject]@{ + Arquivo = $f.Name + Duracao = if ($dur -gt 0) { "{0:hh\:mm\:ss}" -f [TimeSpan]::FromSeconds($dur) } else { '' } + Resolucao = if ($w -and $h) { "${w}x${h}" } else { '' } + Codecs = ($codecs -join '/') + Bitrate = $brTxt + Tamanho = (Tam $f.Length) + Caminho = $f.FullName + } +} + +Titulo 'Resultado' +$linhas | Format-Table Arquivo, Duracao, Resolucao, Codecs, Bitrate, Tamanho -AutoSize | Out-String -Width 200 | Write-Host + +$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { $ent.Origem }) +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimaSaida=$destino }) +$csv = Join-Path $destino ("info-midia_{0}.csv" -f (Get-Date -Format 'yyyy-MM-dd_HH-mm')) +try { + $linhas | Export-Csv -LiteralPath $csv -NoTypeInformation -Encoding UTF8 -Delimiter ';' + Write-Host "[OK] Planilha salva em: $csv" -ForegroundColor Green +} catch { Write-Host "[!] Não foi possível salvar o CSV: $($_.Exception.Message)" -ForegroundColor Yellow } +Resumo $linhas.Count 0 0 $destino
@@ -0,0 +1,181 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Juntar Vídeos - concatena vários vídeos num só (FFmpeg) +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Juntar Vídeos' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'JuntarVideos' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Juntar Vídeos' +Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null +$config = Carregar-Config + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$pasta = $null +while (-not $pasta) { + Titulo 'Pasta com os vídeos' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | os vídeos são juntados em ordem alfabética' -ForegroundColor DarkGray + $r = Read-Host 'Pasta' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Container) { $pasta = (Resolve-Path -LiteralPath $c).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $c } +} +$videos = @(Get-ChildItem -LiteralPath $pasta -File | Where-Object { $extVideo -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +if ($videos.Count -lt 2) { Write-Host 'São necessários pelo menos 2 vídeos nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 } + +Titulo 'Ordem' +for ($i=0; $i -lt $videos.Count; $i++) { Write-Host (" {0,2}. {1}" -f ($i+1), $videos[$i].Name) -ForegroundColor Gray } +Write-Host '' +$ok = Read-Host "Juntar esses $($videos.Count) vídeos nessa ordem? (s/N)" +if ($ok -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 } + +Titulo 'Modo' +Write-Host '> [1] Rápido - sem recodificar (exige que sejam do mesmo tipo/codec)' -ForegroundColor White +Write-Host ' [2] Seguro - recodifica tudo para um padrão (sempre funciona, demora)' -ForegroundColor Gray +$m = Read-Host 'Modo (Enter = 1)' +$recodificar = ($m.Trim() -eq '2') + +$nome = (Read-Host 'Nome do arquivo final (Enter = juntado.mp4)').Trim() +if ([string]::IsNullOrWhiteSpace($nome)) { $nome = 'juntado.mp4' } +if ($nome -notmatch '\.\w+$') { $nome += '.mp4' } + +$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { $pasta }) +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta; UltimaSaida=$destino }) +$saida = Join-Path $destino $nome + +$lista = Join-Path $env:TEMP ("juntar_{0}.txt" -f ([guid]::NewGuid().ToString('N'))) +$sb = New-Object System.Text.StringBuilder +foreach ($v in $videos) { [void]$sb.AppendLine("file '" + ($v.FullName -replace '\\', '/') + "'") } +[System.IO.File]::WriteAllText($lista, $sb.ToString(), (New-Object System.Text.UTF8Encoding($false))) + +$ffArgs = @('-hide_banner','-nostdin','-loglevel','error','-f','concat','-safe','0','-i',$lista) +if ($recodificar) { $ffArgs += @('-c:v','libx264','-preset','veryfast','-crf','20','-c:a','aac','-b:a','160k') } +else { $ffArgs += @('-c','copy') } +$ffArgs += @('-movflags','+faststart','-y',$saida) + +Titulo 'Juntando' +Write-Host "Saída: $saida" +Write-Host '' +& ffmpeg @ffArgs +$rc = $LASTEXITCODE +Remove-Item -LiteralPath $lista -Force -ErrorAction SilentlyContinue + +if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) { + Write-Host ("[OK] {0} vídeos juntados ({1})." -f $videos.Count, (Tam (Get-Item $saida).Length)) -ForegroundColor Green + Resumo 1 0 0 $destino +} else { + Write-Host '[ERRO] Falhou. Se usou o modo rápido, tente o modo seguro (opção 2).' -ForegroundColor Red + if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue } + Resumo 0 0 1 $destino +}
@@ -0,0 +1,350 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Marca d'água em Vídeo - aplica texto ou logo sobre vídeos (FFmpeg) +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Marca d''água em Vídeo' } catch {} + +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'MarcaDaguaVideo' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir "Marca d'água em Vídeo" +Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null +$config = Carregar-Config + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$ent = Pedir-Entrada $padrao $extVideo 'Pasta ou vídeo' +$itens = Listar $ent $extVideo +if ($itens.Count -eq 0) { Write-Host 'Nenhum vídeo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } + +Titulo "Tipo de marca d'água" +Write-Host ' [1] Texto' -ForegroundColor White +Write-Host ' [2] Logo (arquivo PNG, de preferência com fundo transparente)' -ForegroundColor White +$tipo = $null +while (-not $tipo) { $t = Read-Host 'Tipo (1-2)'; switch ($t.Trim()) { '1' { $tipo='texto' } '2' { $tipo='logo' } default { Write-Host '[!] Digite 1 ou 2.' -ForegroundColor Yellow } } } + +$texto = $null; $logo = $null; $pct = 20 +if ($tipo -eq 'texto') { + while ([string]::IsNullOrWhiteSpace($texto)) { $texto = Read-Host 'Texto da marca' } +} else { + while (-not $logo) { + $l = (Read-Host 'Caminho do logo (PNG)').Trim().Trim('"') + if (Test-Path -LiteralPath $l -PathType Leaf) { $logo = (Resolve-Path -LiteralPath $l).Path } else { Write-Host '[!] Arquivo não encontrado.' -ForegroundColor Yellow } + } + $p = Read-Host 'Largura do logo em % do vídeo (Enter = 20)' + if (-not [string]::IsNullOrWhiteSpace($p) -and ($p -as [int])) { $pct = [int]$p } +} + +$posicoes = @( + [pscustomobject]@{ Id=1; Nome='Inferior direita'; XY='W-w-20:H-h-20'; TXY='w-tw-20:y=h-th-20' }, + [pscustomobject]@{ Id=2; Nome='Inferior esquerda'; XY='20:H-h-20'; TXY='20:y=h-th-20' }, + [pscustomobject]@{ Id=3; Nome='Superior direita'; XY='W-w-20:20'; TXY='w-tw-20:y=20' }, + [pscustomobject]@{ Id=4; Nome='Superior esquerda'; XY='20:20'; TXY='20:y=20' }, + [pscustomobject]@{ Id=5; Nome='Centro'; XY='(W-w)/2:(H-h)/2'; TXY='(w-tw)/2:y=(h-th)/2' } +) +Titulo 'Posição' +foreach ($p in $posicoes) { Write-Host (" [{0}] {1}" -f $p.Id, $p.Nome) -ForegroundColor Gray } +$pos = $null +while (-not $pos) { $e = Read-Host 'Posição (1-5, Enter = 1)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = '1' }; $pos = $posicoes | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1; if (-not $pos) { Write-Host '[!] Inválido.' -ForegroundColor Yellow } } + +$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'ComMarca' }) +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimoTipo=$tipo; UltimoTexto=$texto; UltimoLogo=$logo; UltimaPosicao=$pos.Id; UltimaSaida=$destino }) + +# O drawtext do FFmpeg nao tem fonte padrao no Windows: e' preciso apontar uma. +$fonteArq = $null +foreach ($cand in @('C:\Windows\Fonts\arial.ttf','C:\Windows\Fonts\segoeui.ttf','C:\Windows\Fonts\calibri.ttf','C:\Windows\Fonts\tahoma.ttf')) { + if (Test-Path -LiteralPath $cand) { $fonteArq = $cand; break } +} +if (-not $fonteArq -and $tipo -eq 'texto') { + Write-Host '[ERRO] Nenhuma fonte encontrada em C:\Windows\Fonts para desenhar o texto.' -ForegroundColor Red + Pausar; exit 1 +} +# no filtro do ffmpeg o caminho vai com / e o ':' da unidade precisa ser escapado +$fonteFf = if ($fonteArq) { ($fonteArq -replace '\\', '/') -replace '^([A-Za-z]):', '$1\:' } else { '' } +$temNvenc = [bool](& ffmpeg -hide_banner -encoders 2>$null | Select-String 'h264_nvenc') +$vcodec = if ($temNvenc) { @('-c:v','h264_nvenc','-preset','p5','-cq','23') } else { @('-c:v','libx264','-preset','veryfast','-crf','21') } + +$ok=0;$ign=0;$err=0 +$tarefas = @() +foreach ($f in $itens) { + $saida = Join-Path $destino ($f.BaseName + '.mp4') + if (Test-Path -LiteralPath $saida) { $ign++; continue } + if ($tipo -eq 'texto') { + $txt = $texto -replace '\\', '\\\\' -replace ':', '\:' -replace "'", "" + $filtro = "drawtext=fontfile='$fonteFf':text='$txt':fontcolor=white@0.85:fontsize=h/22:box=1:boxcolor=black@0.35:boxborderw=8:x=$($pos.TXY)" + $a = @('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName,'-vf',$filtro) + $vcodec + @('-c:a','copy','-y',$saida) + } else { + $filtro = "[1][0]scale2ref=w=iw*$pct/100:h=ow/mdar[logo][base];[base][logo]overlay=$($pos.XY)" + $a = @('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName,'-i',$logo,'-filter_complex',$filtro) + $vcodec + @('-c:a','copy','-y',$saida) + } + $tarefas += @{ Exe='ffmpeg'; Rotulo=$f.Name; Alvo=$saida; Args=$a } +} +if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow } + +Titulo 'Aplicando' +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao $(if ($temNvenc) { 'gpu' } else { 'cpu' }) + Write-Host ("{0} vídeo(s), até {1} ao mesmo tempo ({2})..." -f $tarefas.Count, $lim, $(if ($temNvenc) { 'GPU NVENC' } else { 'CPU' })) -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + Relatar $rs $tarefas ([ref]$ok) ([ref]$err) +} +Resumo $ok $ign $err $destino
@@ -0,0 +1,323 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Normalizar Áudio - padroniza o volume em lote, EBU R128 (FFmpeg) +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Normalizar Áudio' } catch {} + +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'NormalizarAudio' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Normalizar Áudio' +Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null +$config = Carregar-Config + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$ent = Pedir-Entrada $padrao ($extVideo + $extAudio) 'Pasta ou arquivo' +$itens = Listar $ent ($extVideo + $extAudio) +if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 } + +$alvos = @( + [pscustomobject]@{ Id=1; Nome='Streaming / YouTube (-14 LUFS)'; I=-14 }, + [pscustomobject]@{ Id=2; Nome='Podcast / voz (-16 LUFS)'; I=-16 }, + [pscustomobject]@{ Id=3; Nome='TV / broadcast (-23 LUFS)'; I=-23 } +) +$padraoId = if ($config -and $config.UltimoAlvo) { [int]$config.UltimoAlvo } else { 1 } +Titulo 'Volume alvo' +foreach ($a in $alvos) { + $m = if ($a.Id -eq $padraoId) { '>' } else { ' ' } + $c = if ($a.Id -eq $padraoId) { 'White' } else { 'Gray' } + Write-Host ("{0} [{1}] {2}" -f $m, $a.Id, $a.Nome) -ForegroundColor $c +} +Write-Host "Enter = usar a opção padrão [$padraoId]" -ForegroundColor DarkGray +$alvo = $null +while (-not $alvo) { + $e = Read-Host 'Alvo (1-3)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = "$padraoId" } + $alvo = $alvos | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1 + if (-not $alvo) { Write-Host '[!] Escolha de 1 a 3.' -ForegroundColor Yellow } +} + +$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'Normalizado' }) +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimoAlvo=$alvo.Id; UltimaSaida=$destino }) + +$ok=0;$ign=0;$err=0 +$tarefas = @() +foreach ($f in $itens) { + $saida = Join-Path $destino $f.Name + if (Test-Path -LiteralPath $saida) { $ign++; continue } + $ehVideo = $extVideo -contains $f.Extension.ToLowerInvariant() + $a = @('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName, + '-af',"loudnorm=I=$($alvo.I):TP=-1.5:LRA=11") + if ($ehVideo) { $a += @('-c:v','copy') } + $a += @('-y',$saida) + $tarefas += @{ Exe='ffmpeg'; Rotulo=$f.Name; Alvo=$saida; Args=$a } +} +if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow } + +Titulo 'Normalizando' +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao 'cpu' + Write-Host ("{0} arquivo(s), até {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + Relatar $rs $tarefas ([ref]$ok) ([ref]$err) +} +Resumo $ok $ign $err $destino
@@ -12,6 +12,155 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = 'Transcodificar Vídeos' } catch {} +# ============================================================ +# Executar-EmParalelo - pool de processos externos (PS 5.1) +# Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 } +# Dur e' opcional (segundos) e so' pondera a barra de progresso. +# Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos +# ============================================================ +function Limite-Padrao([string]$tipo) { + $n = [Environment]::ProcessorCount + switch ($tipo) { + 'gpu' { 3 } + 'cpu' { [Math]::Max(2, [Math]::Min(4, [int]($n / 6))) } + 'imagem' { [Math]::Max(2, [Math]::Min(8, [int]($n / 3))) } + 'sonda' { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) } + default { 4 } + } +} + +function Executar-EmParalelo { + param( + [object[]]$Tarefas, + [int]$Limite = 4, + [switch]$SemProgresso + ) + + # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as + # barras finais e' o que impede um caminho terminado em '\' de comer + # a aspa de fechamento. + function _Citar([string]$a) { + if ($a -eq '') { return '""' } + if ($a -notmatch '[\s"]') { return $a } + $s = ''; $bs = 0 + foreach ($c in $a.ToCharArray()) { + if ($c -eq '\') { $bs++; continue } + if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue } + if ($bs) { $s += ('\' * $bs); $bs = 0 } + $s += $c + } + if ($bs) { $s += ('\' * ($bs * 2)) } + return '"' + $s + '"' + } + + $tot = @($Tarefas).Count + if ($tot -eq 0) { return @() } + if ($Limite -lt 1) { $Limite = 1 } + + $res = New-Object object[] $tot + $ativos = New-Object System.Collections.ArrayList + $prox = 0; $feitos = 0; $falhas = 0 + $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } } + $durFeita = 0.0 + $t0 = Get-Date + + try { + while ($feitos -lt $tot) { + + while ($ativos.Count -lt $Limite -and $prox -lt $tot) { + $t = $Tarefas[$prox] + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $t.Exe + $psi.Arguments = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ') + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8 + $psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8 + $p = [System.Diagnostics.Process]::Start($psi) + [void]$ativos.Add([pscustomobject]@{ + Idx = $prox + Rot = $t.Rotulo + Dur = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 }) + Proc = $p + Linha = $p.StandardOutput.ReadLineAsync() + Err = $p.StandardError.ReadToEndAsync() + Buf = (New-Object System.Text.StringBuilder) + Seg = 0.0 + Ini = (Get-Date) + }) + $prox++ + } + + # drena stdout sem bloquear (senao o pipe cheio trava o processo) + foreach ($a in $ativos) { + while ($a.Linha -and $a.Linha.IsCompleted) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') { + $a.Seg = [double]$matches[1] / 1000000.0 + } + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + } + + for ($i = $ativos.Count - 1; $i -ge 0; $i--) { + $a = $ativos[$i] + if (-not $a.Proc.HasExited) { continue } + while ($a.Linha) { + $l = $a.Linha.Result + if ($null -eq $l) { $a.Linha = $null; break } + [void]$a.Buf.AppendLine($l) + $a.Linha = $a.Proc.StandardOutput.ReadLineAsync() + } + $rc = $a.Proc.ExitCode + $res[$a.Idx] = [pscustomobject]@{ + Indice = $a.Idx + Rotulo = $a.Rot + Codigo = $rc + Saida = $a.Buf.ToString() + Erro = $a.Err.Result + Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1) + } + $durFeita += $a.Dur + try { $a.Proc.Dispose() } catch {} + $ativos.RemoveAt($i) + $feitos++ + if ($rc -ne 0) { $falhas++ } + } + + if (-not $SemProgresso) { + if ($durTotal -gt 0) { + $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg } + $frac = $parcial / $durTotal + } else { + $frac = $feitos / $tot + } + if ($frac -gt 1) { $frac = 1 } + $pct = [int]($frac * 100) + $dec = (Get-Date) - $t0 + $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero } + $ch = [int]($pct / 5) + Write-Host ("`r [{0}] {1,3}% {2}/{3} ativos:{4} falhas:{5} restante ~{6:mm\:ss} " -f ` + (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) ` + -NoNewline -ForegroundColor Cyan + } + + if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 } + } + } + finally { + foreach ($a in $ativos) { + try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {} + try { $a.Proc.Dispose() } catch {} + } + } + if (-not $SemProgresso) { Write-Host '' } + return $res +} + function Pausar { Write-Host '' Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray@@ -259,64 +408,66 @@
$convertidos = 0 $ignorados = 0 $erros = 0 -$indice = 0 +# --- 1) sonda as duracoes em paralelo (alimenta a barra global) --- +$sonda = @() foreach ($arquivo in $arquivos) { - $indice++ + $sonda += @{ Exe='ffprobe'; Rotulo=$arquivo.Name + Args=@('-v','error','-show_entries','format=duration','-of','csv=p=0',$arquivo.FullName) } +} +$duracoes = @{} +if ($sonda.Count -gt 0) { + Write-Host 'Analisando os videos...' -ForegroundColor Gray + $rs = Executar-EmParalelo -Tarefas $sonda -Limite (Limite-Padrao 'sonda') -SemProgresso + for ($i = 0; $i -lt $rs.Count; $i++) { + $d = 0.0 + if ($rs[$i] -and $rs[$i].Codigo -eq 0) { + $bruto = (("$($rs[$i].Saida)" -split "`n" | Where-Object { $_.Trim() } | Select-Object -First 1) -replace ',', '.') + [double]::TryParse($bruto, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d) | Out-Null + } + $duracoes[$arquivos[$i].FullName] = $d + } +} +# --- 2) monta as tarefas --- +switch ($motor) { + 'gpu' { $vcodec = @('-c:v','h264_nvenc','-preset','p5','-cq',"$($perfil.Crf)",'-pix_fmt','yuv420p') } + 'cpu-rapido' { $vcodec = @('-c:v','libx264','-preset','veryfast','-crf',"$($perfil.Crf)",'-pix_fmt','yuv420p') } + default { $vcodec = @('-c:v','libx264','-preset','medium','-crf',"$($perfil.Crf)",'-pix_fmt','yuv420p') } +} +$tarefas = @() +foreach ($arquivo in $arquivos) { $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 - } - - # Codec de vídeo conforme o motor escolhido - switch ($motor) { - 'gpu' { $vcodec = @('-c:v', 'h264_nvenc', '-preset', 'p5', '-cq', "$($perfil.Crf)", '-pix_fmt', 'yuv420p') } - 'cpu-rapido' { $vcodec = @('-c:v', 'libx264', '-preset', 'veryfast', '-crf', "$($perfil.Crf)", '-pix_fmt', 'yuv420p') } - default { $vcodec = @('-c:v', 'libx264', '-preset', 'medium', '-crf', "$($perfil.Crf)", '-pix_fmt', 'yuv420p') } - } - $ffArgs = @('-hide_banner', '-nostdin', '-i', $arquivo.FullName, '-map', '0:v:0', '-map', '0:a?', '-vf', $vf) + + if (Test-Path -LiteralPath $saida) { $ignorados++; continue } + $ffArgs = @('-hide_banner','-nostdin','-loglevel','error','-i',$arquivo.FullName,'-map','0:v:0','-map','0:a?','-vf',$vf) + $vcodec + - @('-c:a', 'aac', '-b:a', $perfil.Audio, '-ac', '2', '-movflags', '+faststart', '-progress', 'pipe:1', '-nostats', '-y', $saida) + @('-c:a','aac','-b:a',$perfil.Audio,'-ac','2','-movflags','+faststart','-progress','pipe:1','-nostats','-y',$saida) + $tarefas += @{ Exe='ffmpeg'; Rotulo=$arquivo.Name; Alvo=$saida; Dur=$duracoes[$arquivo.FullName]; Args=$ffArgs } +} - # Duração do vídeo para a barra de progresso - $dur = 0.0 - try { $dur = [double](& ffprobe -v error -show_entries format=duration -of csv=p=0 $arquivo.FullName 2>$null) } catch {} +if ($ignorados -gt 0) { Write-Host "$ignorados ja existiam no destino e foram pulados." -ForegroundColor Yellow } - & ffmpeg @ffArgs 2>$null | ForEach-Object { - if ($dur -gt 0 -and $_ -match '^out_time=(\d+):(\d+):([\d.]+)') { - $seg = [int]$matches[1]*3600 + [int]$matches[2]*60 + [double]$matches[3] - $pct = [int](($seg / $dur) * 100); if ($pct -gt 100) { $pct = 100 } - $cheio = [int]($pct / 5) - $barra = ('#' * $cheio) + ('-' * (20 - $cheio)) - Write-Host ("`r [{0}] {1,3}% " -f $barra, $pct) -NoNewline -ForegroundColor Cyan +# --- 3) converte em paralelo --- +if ($tarefas.Count -gt 0) { + $lim = Limite-Padrao $(if ($motor -eq 'gpu') { 'gpu' } else { 'cpu' }) + Write-Host ("Convertendo {0} video(s), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray + $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $convertidos++ } + else { + $erros++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } } } - $rc = $LASTEXITCODE - if ($dur -gt 0) { Write-Host ("`r [{0}] 100% " -f ('#' * 20)) -ForegroundColor Cyan } - - if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) { - 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 # ------------------------------------------------------------
@@ -0,0 +1,170 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Canivete - menu central de todos os programas +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Canivete' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'Canivete' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +$raizCanivete = Split-Path -Parent $MyInvocation.MyCommand.Path +if (-not $raizCanivete) { $raizCanivete = (Get-Location).Path } + +$ordem = @('Vídeos','Imagens','Organizar','Úteis') +$icone = @{ 'Vídeos'='[V]'; 'Imagens'='[I]'; 'Organizar'='[O]'; 'Úteis'='[U]' } + +while ($true) { + Clear-Host + Titulo 'CANIVETE - todos os programas' + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + + $itens = @() + $n = 0 + foreach ($cat in $ordem) { + $dir = Join-Path $raizCanivete $cat + if (-not (Test-Path -LiteralPath $dir)) { continue } + $cmds = @(Get-ChildItem -LiteralPath $dir -File -Filter *.cmd | Sort-Object Name) + if ($cmds.Count -eq 0) { continue } + Write-Host '' + Write-Host (" {0} {1}" -f $icone[$cat], $cat.ToUpperInvariant()) -ForegroundColor Cyan + foreach ($c in $cmds) { + $n++ + $itens += $c + $nome = $c.BaseName -replace '-', ' ' + Write-Host (" {0,2}. {1}" -f $n, $nome) -ForegroundColor Gray + } + } + if ($itens.Count -eq 0) { + Write-Host '' + Write-Host '[!] Nenhum programa encontrado. Coloque o canivete.cmd na pasta raiz da coleção.' -ForegroundColor Yellow + Pausar; exit 1 + } + + Write-Host '' + Write-Host ' 0. Sair' -ForegroundColor DarkGray + Write-Host '' + $e = Read-Host "Abrir qual? (1-$($itens.Count), 0 = sair)" + if ([string]::IsNullOrWhiteSpace($e) -or $e.Trim() -eq '0') { break } + $i = $e -as [int] + if ($i -ge 1 -and $i -le $itens.Count) { + $alvo = $itens[$i-1] + Write-Host '' + Write-Host ("Abrindo {0}..." -f $alvo.BaseName) -ForegroundColor Green + try { Start-Process -FilePath $alvo.FullName | Out-Null } + catch { Write-Host "[ERRO] Não foi possível abrir: $($_.Exception.Message)" -ForegroundColor Red; Start-Sleep -Seconds 2 } + Start-Sleep -Milliseconds 600 + } else { + Write-Host '[!] Opção inválida.' -ForegroundColor Yellow + Start-Sleep -Seconds 1 + } +}
@@ -0,0 +1,24 @@
+# Dependências + +A maioria dos programas **não precisa de nada**. Os que precisam avisam na tela e mostram o comando de instalação. + +## Tabela + +| Ferramenta | Para que serve | Como instalar | Usada por | +|---|---|---|---| +| **FFmpeg** | Tudo de vídeo e áudio | `winget install Gyan.FFmpeg` | transcodificar, extrair-audio, criar-gif, cortar-video, juntar-videos, marca-dagua-video, info-midia, normalizar-audio, baixar-videos, limpar-metadados | +| **ImageMagick** | Tudo de imagem | `winget install ImageMagick.ImageMagick` | converter-imagens, marca-dagua, imagens-e-pdf, limpar-metadados, renomear-em-lote (data da foto) | +| **Ghostscript** | Ler e comprimir PDF | `winget install ArtifexSoftware.GhostScript` | comprimir-pdf, imagens-e-pdf (PDF → imagens) | +| **yt-dlp** | Baixar de sites | *baixado automaticamente* | baixar-videos | +| **7-Zip** *(opcional)* | `.rar`, `.7z` e mais velocidade | `winget install 7zip.7zip` | compactar-zip | +| **smartmontools** *(opcional)* | Detalhes SMART avançados | `winget install smartmontools` | verificar-discos | + +## Observações + +- **O yt-dlp se instala sozinho.** Se não existir na máquina, o `baixar-videos` baixa a versão oficial mais recente em `%LOCALAPPDATA%\fast-scripts` e ainda a mantém atualizada. +- **O Ghostscript costuma não ficar no PATH.** Os programas procuram também em `C:\Program Files\gs`, então funciona mesmo assim. +- **Placa NVIDIA** não é uma dependência, mas é aproveitada automaticamente quando existe (transcodificar e marca-dagua-video). +- Programas que **não precisam de nada**: organizar-por-tipo, achar-duplicados, limpar-nomes, backup-espelhado, analisar-espaco, verificar-integridade, relatorio-sistema, limpeza-segura, verificar-discos, canivete. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,104 @@
+# Padrões do projeto + +Contrato técnico da coleção — leia antes de criar ou alterar qualquer programa. + +## 1. Um arquivo, autocontido + +Cada programa é **um único `.cmd`**, sem depender de nenhum outro arquivo. Os programas ficam num compartilhamento de rede e podem ser copiados isoladamente, então **não existe biblioteca compartilhada**: o código comum é duplicado de propósito, mas deve ser **idêntico** em todos. + +## 2. Cabeçalho polyglot + +O arquivo é batch e PowerShell ao mesmo tempo. O batch relança o próprio arquivo no PowerShell: + +``` +<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> +``` + +**Variante para quem precisa de administrador** (`verificar-discos`, `limpeza-segura`): expõe o próprio caminho numa variável, porque o script precisa se relançar elevado. + +``` +<# : +@echo off +set "SELF=%~f0" +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText($env:SELF,[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> +``` + +## 3. Codificação — a regra que mais quebra + +Os `.cmd` **precisam** ser gravados em **UTF-8 sem BOM** com quebras **CRLF**. + +- Com BOM, o batch não reconhece a primeira linha e o duplo-clique falha. +- Com LF, o `cmd.exe` se perde no meio do arquivo. + +Depois de qualquer edição, normalize: + +```powershell +$t = [System.IO.File]::ReadAllText($p, [System.Text.Encoding]::UTF8) +$t = $t -replace "`r`n","`n" -replace "`n","`r`n" +[System.IO.File]::WriteAllText($p, $t, (New-Object System.Text.UTF8Encoding($false))) +``` + +Os primeiros bytes devem ser `3C 23 20 3A 0D`. + +> **Atenção:** para scripts `.ps1` auxiliares vale o **contrário** — o Windows PowerShell 5.1 lê `.ps1` sem BOM como ANSI e destrói os acentos. Esses devem ter BOM. + +## 4. Verificação obrigatória + +Antes de considerar qualquer arquivo pronto: + +```powershell +$e = $null; $tok = $null +[void][System.Management.Automation.Language.Parser]::ParseInput($t, [ref]$tok, [ref]$e) +``` + +E rodar de verdade, alimentando os prompts por `stdin`, numa pasta de teste. + +## 5. Experiência padrão + +1. Verificar as dependências primeiro e avisar com o comando `winget` se faltar. +2. Perguntar **pasta ou arquivo** (aceitar os dois). +3. Menus com `>` marcando a opção padrão; Enter aceita o padrão. +4. Perguntar a **pasta de saída**, sugerindo a última usada e **criando-a se não existir**. +5. Lembrar as escolhas em `%APPDATA%\<Nome>\config.json`. +6. Preservar subpastas e **pular o que já foi feito**. +7. Nada destrutivo sem **prévia + confirmação**. +8. Resumo no fim e `Pausar` antes de fechar. +9. Assinatura `Desenvolvido por Pablo Murad - 2026` no comentário do cabeçalho **e** na tela. + +## 6. Paralelismo + +Programas que processam lotes usam a função `Executar-EmParalelo`, colada dentro de cada `.cmd`. + +**Pontos que não podem ser simplificados:** + +- Usa `System.Diagnostics.Process`. **Nunca** `Start-Process -ArgumentList` (quebra o quoting) nem `Start-Job` (~125 ms de overhead por item). +- O helper `_Citar` implementa as regras do `CommandLineToArgvW`. A regra crítica é **dobrar as barras finais** — sem isso, uma pasta colada do Explorer terminada em `\` engole a aspa de fechamento. +- Redirecionar **stdout e stderr**. Não redirecionar o stderr trava o ffmpeg quando o buffer do pipe enche. +- Ler `$p.ExitCode` por processo — `$LASTEXITCODE` é global e não sobrevive ao paralelismo. Testar `-ne 0`, nunca `-gt 0` (o ffmpeg devolve `-2`). + +**Limites medidos nesta máquina (24 núcleos, RTX 5070 Ti):** + +| Tipo | Limite | Ganho medido | +|------|--------|--------------| +| `imagem` | 8 | até **4,2×** | +| `cpu` (x264) | 4 | **1,57×** — com 12 fica *pior* que sequencial | +| `gpu` (NVENC) | 3 | ~10% — o encoder satura sozinho | +| `sonda` (ffprobe) | 12 | elimina N inicializações em série | + +## 7. Armadilhas já encontradas + +- **`Get-FileHash` não está disponível** quando o `.cmd` roda via `Invoke-Expression`. Use `[System.Security.Cryptography.SHA256]` direto. +- **Variáveis do script não resolvem dentro de funções** nesse contexto: passe o que precisar como **parâmetro**. +- **`catch {}` vazio esconde bugs.** Sempre mostre o motivo do erro. +- **O robocopy é traduzido:** em português imprime `*Arquivo EXTRA`, em inglês `*EXTRA File`. Use uma regex que pegue os dois. +- **`Get-Command` devolve `.Source`; `Get-Item` devolve `.FullName`.** Normalize num caminho só. +- **O `drawtext` do FFmpeg não tem fonte padrão no Windows:** é preciso apontar um `fontfile`, com o `:` da unidade escapado. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,23 +1,39 @@
-# Achar duplicados +# Achar Duplicados -Programa que encontra arquivos iguais pelo conteúdo (não só pelo nome) dentro de uma pasta e subpastas. +Encontra arquivos com o **conteúdo** idêntico (mesmo com nomes diferentes) e mostra quanto espaço dá para recuperar. ## Como abrir -Abra o arquivo `achar-duplicados.cmd` na pasta `Organizar` (duplo clique ou Execute). +Dê um duplo-clique em `Organizar\achar-duplicados.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra ## Passo a passo -1. Cole o caminho da pasta a analisar. -2. Aguarde a análise (pastas grandes podem demorar). -3. Se houver duplicados, veja o resumo: grupos de iguais, cópias extras e espaço que dá para recuperar. Até 15 grupos são listados na tela; o primeiro de cada grupo é marcado como manter. -4. Escolha o que fazer: - - **Só listar** (Enter = padrão) — nada é alterado. - - **Mover as cópias extras** para a subpasta `_Duplicados`. - - **Apagar as cópias extras** — pede confirmação digitando `APAGAR`; sempre mantém 1 original. +1. Informe a **pasta** (ele olha também as subpastas). +2. Aguarde a comparação. +3. Veja os grupos de iguais. +4. Escolha o que fazer com as cópias extras. + +## Opções + +| # | Ação | O que faz | +|---|------|-----------| +| 1 | Só listar | Não altera nada — é o padrão | +| 2 | Mover | Move as cópias extras para a subpasta `_Duplicados` | +| 3 | Apagar | Apaga as cópias extras (exige digitar `APAGAR`) | + +## Onde salva + +A opção 2 cria a subpasta `_Duplicados` dentro da pasta analisada. ## Dicas -- Pastas com muitos arquivos iguais de tamanho passam por uma comparação mais profunda — paciência. -- A opção de apagar é permanente; se tiver dúvida, use só listar ou mover. -- Pressione Enter no final para fechar a janela. +- **Sempre mantém uma cópia** de cada arquivo — o primeiro em ordem alfabética, marcado como `[manter]`. +- É rápido porque compara em três etapas: primeiro o tamanho, depois só as pontas do arquivo, e só então o conteúdo inteiro. +- Na dúvida, use a opção 2 (mover): dá para conferir antes de apagar de vez. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,31 @@
+# Analisar Espaço + +Responde "o que está enchendo meu HD?" — mostra o espaço de cada unidade, as maiores subpastas e os maiores arquivos. + +## Como abrir + +Dê um duplo-clique em `Úteis\analisar-espaco.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra + +## Passo a passo + +1. Veja o **espaço de cada unidade** (aparece logo de cara). +2. Informe a **pasta** para analisar em detalhe. +3. Veja as maiores subpastas e os maiores arquivos. + + +## Onde salva + +Nada é salvo — é só leitura. + +## Dicas + +- **Nada é alterado**: o programa só lê. +- Comece pela raiz da unidade (`C:\`) para achar o vilão, depois vá afunilando. +- Combina bem com o [achar-duplicados](achar-duplicados.md) e a [limpeza-segura](limpeza-segura.md). + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,40 @@
+# Backup Espelhado + +Copia uma pasta para outro lugar (HD externo, rede) de forma incremental — só o que mudou — usando o robocopy com vários núcleos. + +## Como abrir + +Dê um duplo-clique em `Úteis\backup-espelhado.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra (o robocopy já vem no Windows) + +## Passo a passo + +1. Informe a **pasta de origem**. +2. Informe a **pasta de destino** (é criada se não existir). +3. Escolha o **modo**. +4. Veja a **prévia** (simulação — nada é copiado ainda). +5. Confirme. + +## Opções + +| # | Modo | Cuidado | +|---|------|---------| +| 1 | Cópia | Só adiciona e atualiza. **Nunca apaga nada** no destino | +| 2 | Espelho | Deixa o destino idêntico à origem — **apaga no destino** o que não existe mais na origem | + +## Onde salva + +Na pasta de destino. O log fica em `%LOCALAPPDATA%\fast-scripts\logs-backup`. + +## Dicas + +- A **prévia é uma simulação real**: mostra quantos arquivos seriam copiados e, no modo espelho, exatamente quais seriam apagados. +- O modo espelho exige digitar **`ESPELHAR`** para confirmar — não dá para apagar sem querer. +- O log fica **fora do destino** de propósito: senão o próprio espelho apagaria os logs antigos. +- É incremental: a segunda execução copia só o que mudou, e é muito mais rápida. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,44 @@
+# Baixar Vídeos + +Baixa vídeos ou só o áudio de YouTube, Instagram, TikTok, X, Vimeo, Facebook e mais de mil outros sites. + +## Como abrir + +Dê um duplo-clique em `Vídeos\baixar-videos.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **yt-dlp** — *o programa baixa sozinho se não existir* (versão oficial mais recente) +- **FFmpeg** — para juntar vídeo+áudio em alta qualidade e converter áudio + +## Passo a passo + +1. Cole o **link** (pode colar vários separados por espaço). +2. Escolha a **qualidade/formato** (1 a 7). +3. Diga se quer a **playlist inteira** ou só o vídeo. +4. Confirme a **pasta de saída**. + +## Opções + +| # | Formato | +|---|---------| +| 1 | Melhor qualidade disponível | +| 2 | 1080p | +| 3 | 720p | +| 4 | 480p | +| 5 | 360p (compacto) | +| 6 | Só áudio — MP3 | +| 7 | Só áudio — M4A | + +## Onde salva + +Na pasta que você escolher (padrão: `Downloads\Baixados`). O nome do arquivo vem do título do vídeo. + +## Dicas + +- Já baixados são **pulados** automaticamente. +- Embute capa e informações (título, autor) no arquivo. +- A cópia do yt-dlp que o programa baixa **se atualiza sozinha** a cada 15 dias. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,27 @@
+# Canivete (menu central) + +O ponto de entrada da coleção: lista todos os programas por categoria e abre o que você escolher. + +## Como abrir + +Dê um duplo-clique em `canivete.cmd`, na pasta raiz. + +## O que precisa + +- Nenhum programa extra + +## Passo a passo + +1. Abra o `canivete.cmd`. +2. Veja a lista numerada, agrupada por categoria. +3. Digite o **número** do programa e tecle Enter — ele abre numa janela própria. +4. O menu continua aberto: dá para abrir outro em seguida. Digite `0` para sair. + +## Dicas + +- A lista é montada **na hora**, varrendo as pastas. Se você adicionar um `.cmd` novo em qualquer categoria, ele aparece sozinho no menu — não precisa editar nada. +- O `canivete.cmd` precisa ficar na **raiz** da coleção (ao lado das pastas `Vídeos`, `Imagens`, `Organizar` e `Úteis`). +- Crie um atalho dele na Área de Trabalho para ter tudo a um clique. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,31 @@
+# Compactar / Extrair + +Compacta uma pasta num ZIP, ou extrai vários arquivos compactados de uma vez. + +## Como abrir + +Dê um duplo-clique em `Organizar\compactar-zip.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra (usa o compactador do Windows) +- *(opcional)* **7-Zip** — `winget install 7zip.7zip` — mais rápido e abre `.rar` e `.7z` + +## Passo a passo + +1. Escolha **[1] Compactar** ou **[2] Extrair**. +2. Informe a pasta (ou os arquivos compactados). +3. Dê o nome do arquivo e confirme a **pasta de saída**. + + +## Onde salva + +Na pasta de saída escolhida. Ao extrair, cada arquivo vai para uma subpasta com o nome dele. + +## Dicas + +- Sem o 7-Zip, só funciona com `.zip`. Com ele, abre também `.rar`, `.7z` e `.tar`. +- O 7-Zip usa todos os núcleos do processador, então é bem mais rápido em pastas grandes. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,39 @@
+# Comprimir PDF + +Diminui o tamanho de arquivos PDF, mostrando quanto cada um encolheu. + +## Como abrir + +Dê um duplo-clique em `Imagens\comprimir-pdf.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **Ghostscript** — `winget install ArtifexSoftware.GhostScript` + *(o programa acha o Ghostscript mesmo que ele não esteja no PATH)* + +## Passo a passo + +1. Informe a **pasta ou um PDF**. +2. Escolha o **nível de compressão**. +3. Confirme a **pasta de saída**. + +## Opções + +| # | Nível | Resolução das imagens | +|---|-------|-----------------------| +| 1 | Tela | 72 dpi — menor arquivo | +| 2 | E-book | 150 dpi — equilibrado | +| 3 | Impressão | 300 dpi | +| 4 | Gráfica | máxima qualidade | + +## Onde salva + +Na pasta de saída escolhida (padrão: subpasta `Comprimido`). + +## Dicas + +- No fim aparece uma tabela **antes → depois** com a porcentagem de redução de cada arquivo. +- PDFs cheios de imagens encolhem muito; PDFs só de texto quase não mudam. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,45 +1,43 @@
# Converter Imagens -Programa para redimensionar e/ou converter imagens, uma de cada vez ou várias de uma pasta. +Redimensiona e converte imagens em lote, sempre mantendo a proporção. ## Como abrir -Abra o arquivo `converter-imagens.cmd` na pasta `Imagens` (duplo clique ou Execute). +Dê um duplo-clique em `Imagens\converter-imagens.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa -Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo. +- **ImageMagick** — `winget install ImageMagick.ImageMagick` ## Passo a passo -1. **Pasta ou imagem** - Confirme o caminho sugerido (Enter) ou cole/digite o caminho de uma pasta ou de uma imagem. - Se a pasta não existir, o programa pergunta se quer criá-la. +1. Informe a **pasta ou uma imagem**. +2. Escolha o **formato de saída**. +3. Escolha como informar o **tamanho**. +4. Confirme a **pasta de saída**. -2. **Formato de saída** - Escolha o número da opção: - - manter o formato original (só redimensiona) - - JPG - - PNG - - WebP +## Opções - Enter usa a opção marcada como padrão (a última que você usou, quando houver). +**Formato:** manter o original · JPG · PNG · WebP -3. **Tamanho** - Escolha: - - pixels do maior lado (ex.: 1920) - - porcentagem (ex.: 50) - - não redimensionar (só muda o formato) +**Tamanho** - Depois informe o valor, se for o caso. Enter pode repetir o valor usado da última vez. +| # | Modo | Exemplo | +|---|------|---------| +| 1 | Pixels do maior lado | `1920` — a imagem cabe em 1920×1920 | +| 2 | Porcentagem | `50` — metade do tamanho | +| 3 | Não redimensionar | Só converte o formato | -4. **Aguarde o processamento** - O programa lista cada imagem e mostra se foi convertida, ignorada (já existia no destino) ou se deu erro. +## Onde salva -5. **Resultado** - Os arquivos ficam na pasta `Convertido`, dentro da pasta de origem (ou ao lado da imagem única). A estrutura de subpastas é preservada. +Na pasta de saída escolhida (padrão: subpasta `Convertido`), preservando as subpastas. ## Dicas -- Você pode processar uma pasta inteira (incluindo subpastas) ou só um arquivo. -- Formatos comuns de entrada são aceitos (fotos e imagens do dia a dia). -- O programa lembra pasta, formato e tamanho usados nas últimas execuções. -- Pressione Enter no final para fechar a janela. +- **Nunca amplia** a imagem — só reduz. +- Converter PNG com transparência para JPG achata sobre fundo branco automaticamente. +- É o programa que mais ganha com o processamento paralelo: lotes grandes ficam **várias vezes mais rápidos**. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,38 @@
+# Cortar Vídeo + +Recorta um trecho do vídeo informando início e fim. No modo rápido é praticamente instantâneo, porque não recodifica nada. + +## Como abrir + +Dê um duplo-clique em `Vídeos\cortar-video.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **FFmpeg** — `winget install Gyan.FFmpeg` + +## Passo a passo + +1. Informe o **vídeo** (ou uma pasta, e escolha pelo número). +2. Informe o **início** e o **fim** — aceita `90`, `01:30` ou `00:01:30`. +3. Escolha o **modo** do corte. +4. Confirme a **pasta de saída**. + +## Opções + +| # | Modo | Como funciona | +|---|------|---------------| +| 1 | Rápido | Não recodifica — **instantâneo**. Corta no quadro-chave mais próximo (pode variar alguns décimos) | +| 2 | Exato | Recodifica para cortar no ponto exato. Demora mais | + +## Onde salva + +Na pasta de saída escolhida (padrão: subpasta `Cortado`). O arquivo sai como `<nome>-corte`. + +## Dicas + +- Use o **rápido** em 99% dos casos: um vídeo de 1 hora é cortado em menos de um segundo. +- Só use o **exato** se precisar do frame exato (ex.: sincronizar com áudio). +- Deixe o fim em branco para cortar até o final do vídeo. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,35 +1,32 @@
# Criar GIF / Slideshow -Programa para transformar vídeo em GIF, ou uma pasta de fotos em um vídeo (slideshow). +Dois usos: transformar um vídeo em GIF, ou transformar uma pasta de fotos num vídeo de slideshow. ## Como abrir -Abra o arquivo `criar-gif.cmd` na pasta `Vídeos` (duplo clique ou Execute). +Dê um duplo-clique em `Vídeos\criar-gif.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. -Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo. +## O que precisa -## O que escolher +- **FFmpeg** — `winget install Gyan.FFmpeg` -1. **Vídeo → GIF** -2. **Fotos (pasta) → vídeo (slideshow)** +## Passo a passo ---- +1. Escolha **[1] Vídeo → GIF** ou **[2] Fotos → vídeo**. +2. **GIF:** informe pasta/vídeo, a largura (padrão 480 px) e os quadros por segundo (padrão 12). +3. **Slideshow:** informe a pasta das fotos, quantos segundos cada foto aparece e o nome do vídeo. +4. Confirme a **pasta de saída**. -### Vídeo → GIF -1. Cole o caminho de um vídeo ou de uma pasta com vídeos. -2. Informe a largura do GIF em pixels (Enter = 480). -3. Informe os quadros por segundo (Enter = 12). -4. Os GIFs ficam na pasta `GIF`, ao lado dos vídeos. Arquivos que já existem são ignorados. +## Onde salva -### Fotos → slideshow - -1. Cole o caminho da pasta com as fotos. -2. Informe quantos segundos cada foto aparece (Enter = 2). -3. Digite o nome do vídeo (Enter = `slideshow.mp4`). -4. O arquivo é criado **dentro da mesma pasta**. A ordem das fotos é alfabética. +Na pasta de saída escolhida (padrão: subpasta `GIF` para os GIFs; a própria pasta das fotos para o slideshow). ## Dicas -- GIFs muito longos ou com muita resolução ficam grandes; use largura e FPS menores se precisar. -- Pressione Enter no final para fechar a janela. +- O GIF usa paleta otimizada (duas passagens), o que deixa a imagem bem melhor que um GIF comum. +- GIF é pesado por natureza: largura 480 e 12 fps já dão um bom equilíbrio. +- No slideshow, fotos em pé e deitadas são ajustadas para 1280×720 com bordas. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,34 +1,37 @@
# Extrair Áudio -Programa para tirar o áudio de vídeos, um de cada vez ou vários de uma pasta. +Tira a trilha de áudio de vídeos e salva como MP3, M4A ou WAV. ## Como abrir -Abra o arquivo `extrair-audio.cmd` na pasta `Vídeos` (duplo clique ou Execute). +Dê um duplo-clique em `Vídeos\extrair-audio.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. -Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo. +## O que precisa + +- **FFmpeg** — `winget install Gyan.FFmpeg` ## Passo a passo -1. **Pasta ou vídeo** - Confirme o caminho sugerido (Enter) ou cole/digite uma pasta ou um arquivo de vídeo. +1. Informe a **pasta ou um vídeo**. +2. Escolha o **formato** do áudio. +3. Confirme a **pasta de saída**. -2. **Formato do áudio** - Escolha: - - MP3 — compatível com tudo - - M4A — bom equilíbrio qualidade × tamanho - - WAV — sem compressão (arquivo maior) +## Opções - Enter usa o formato padrão (o último usado, quando houver). +| # | Formato | Observação | +|---|---------|------------| +| 1 | MP3 (192 kbps) | Compatível com tudo | +| 2 | M4A / AAC (192 kbps) | Melhor qualidade no mesmo tamanho | +| 3 | WAV | Sem compressão, arquivo grande | -3. **Aguarde o processamento** - Cada vídeo é listado; arquivos que já existem no destino são ignorados. +## Onde salva -4. **Resultado** - Os áudios ficam na pasta `Áudio`, dentro da pasta de origem. Subpastas são preservadas. +Na pasta de saída escolhida (padrão: subpasta `Áudio`), preservando as subpastas. ## Dicas -- O programa lembra a última pasta e o formato usados. -- Se um vídeo não tiver faixa de áudio, a extração pode falhar. -- Pressione Enter no final para fechar a janela. +- **Muito rápido quando dá para copiar:** se o áudio do vídeo já for do formato pedido (por exemplo M4A de um MP4), ele **copia sem recodificar** — é quase instantâneo. O programa avisa na tela quantos entraram nesse caso. +- Processa vários vídeos ao mesmo tempo. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,35 +1,32 @@
# Imagens e PDF -Programa para juntar imagens em um PDF, ou transformar páginas de PDF em imagens. +Vai nos dois sentidos: junta várias imagens num único PDF, ou transforma cada página de um PDF numa imagem. ## Como abrir -Abra o arquivo `imagens-e-pdf.cmd` na pasta `Imagens` (duplo clique ou Execute). +Dê um duplo-clique em `Imagens\imagens-e-pdf.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. -Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo. +## O que precisa -## O que escolher +- **ImageMagick** — `winget install ImageMagick.ImageMagick` +- **Ghostscript** (só para PDF → imagens) — `winget install ArtifexSoftware.GhostScript` -1. **Imagens → PDF** — junta as imagens de uma pasta em um único PDF. -2. **PDF → Imagens** — salva cada página do PDF como imagem. +## Passo a passo ---- +1. Escolha **[1] Imagens → PDF** ou **[2] PDF → Imagens**. +2. Informe a pasta (ou o PDF). +3. Para PDF → imagens, escolha PNG ou JPG e a qualidade em DPI. +4. Confirme a **pasta de saída**. -### Imagens → PDF -1. Cole o caminho da pasta com as imagens. -2. Confira quantas imagens foram encontradas (ordem alfabética). -3. Digite o nome do PDF (Enter = `saida.pdf`). -4. O arquivo é criado **dentro da mesma pasta**. +## Onde salva -### PDF → Imagens - -1. Cole o caminho de um PDF ou de uma pasta com PDFs. -2. Escolha o formato: PNG (Enter = padrão) ou JPG. -3. Informe a qualidade em DPI (Enter = 150; 300 = alta). -4. As imagens vão para uma pasta com o nome do PDF + `-imagens` (ex.: `documento-imagens`). +O PDF vai para a pasta escolhida. As imagens vão para uma subpasta `<nome do pdf>-imagens`. ## Dicas -- Nas imagens → PDF, a ordem das páginas segue o nome dos arquivos (A–Z). -- Pressione Enter no final para fechar a janela. +- As imagens entram no PDF em **ordem alfabética** — numere os nomes se a ordem importa. +- 150 DPI serve para ler na tela; use 300 DPI se for imprimir. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,30 @@
+# Info de Mídia + +Mostra uma tabela com codec, resolução, duração, bitrate e tamanho de todos os vídeos e áudios de uma pasta — e salva tudo numa planilha. + +## Como abrir + +Dê um duplo-clique em `Vídeos\info-midia.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **FFmpeg / FFprobe** — `winget install Gyan.FFmpeg` + +## Passo a passo + +1. Informe a **pasta ou um arquivo**. +2. Ele lê tudo em paralelo e mostra a tabela. +3. Confirme a **pasta de saída** para salvar a planilha. + + +## Onde salva + +Um arquivo `info-midia_<data>.csv` na pasta de saída escolhida (separado por `;`, abre direto no Excel). + +## Dicas + +- Ótimo para descobrir **por que um vídeo está pesado** antes de transcodificar. +- Também serve para conferir se um lote saiu na resolução certa. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,37 @@
+# Juntar Vídeos + +Emenda vários vídeos de uma pasta num arquivo só, na ordem alfabética dos nomes. + +## Como abrir + +Dê um duplo-clique em `Vídeos\juntar-videos.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **FFmpeg** — `winget install Gyan.FFmpeg` + +## Passo a passo + +1. Informe a **pasta** com os vídeos. +2. Confira a **ordem** mostrada na tela e confirme. +3. Escolha o **modo** (rápido ou seguro). +4. Dê o **nome** do arquivo final e confirme a pasta de saída. + +## Opções + +| # | Modo | Quando usar | +|---|------|-------------| +| 1 | Rápido | Vídeos do mesmo tipo/codec — não recodifica, é quase instantâneo | +| 2 | Seguro | Vídeos diferentes entre si — recodifica tudo para um padrão. Sempre funciona | + +## Onde salva + +Na pasta de saída escolhida (padrão: a própria pasta dos vídeos). + +## Dicas + +- Para controlar a ordem, renomeie com números na frente: `01-intro.mp4`, `02-meio.mp4`... (o [limpar-nomes](limpar-nomes.md) e o [renomear-em-lote](renomear-em-lote.md) ajudam). +- Se o modo rápido falhar, é sinal de que os vídeos são diferentes entre si: use o modo seguro. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,31 @@
+# Limpar Metadados + +Remove os dados escondidos dos arquivos — localização GPS, modelo da câmera, data, programa usado. Importante antes de publicar fotos na internet. + +## Como abrir + +Dê um duplo-clique em `Organizar\limpar-metadados.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **ImageMagick** — `winget install ImageMagick.ImageMagick` +- *(opcional)* **FFmpeg** — para limpar também os vídeos + +## Passo a passo + +1. Informe a **pasta ou um arquivo**. +2. Veja a **prévia** do que existe hoje numa amostra. +3. Confirme a **pasta de saída**. + + +## Onde salva + +Na pasta de saída escolhida (padrão: subpasta `SemMetadados`). + +## Dicas + +- **Os originais não são alterados** — as cópias limpas vão para a pasta de saída. +- Fotos de celular costumam carregar a **localização exata** de onde foram tiradas. Vale limpar antes de publicar. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,37 @@
+# Limpar Nomes + +Ajeita os nomes dos arquivos para uso na web e em uploads: tira acentos, troca espaços por hífen e deixa tudo em minúsculas. + +## Como abrir + +Dê um duplo-clique em `Organizar\limpar-nomes.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra + +## Passo a passo + +1. Informe a **pasta**. +2. **Confira a prévia** (nome antigo → nome novo). +3. Confirme. + +## Exemplo + +| Antes | Depois | +|-------|--------| +| `Relatório Final (versão 2).pdf` | `relatorio-final-versao-2.pdf` | +| `Foto Açaí — Praia.JPG` | `foto-acai-praia.jpg` | + +## Onde salva + +Renomeia na própria pasta. + +## Dicas + +- Nomes que ficariam repetidos ganham `-1`, `-2`... — nada é sobrescrito. +- Arquivos que já estão com o nome limpo são ignorados. +- Útil antes de subir arquivos para um site, servidor Linux ou repositório. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,21 +1,38 @@
-# Limpeza segura +# Limpeza Segura -Programa que limpa arquivos temporários do sistema e, se quiser, esvazia a Lixeira — sempre com prévia antes de apagar. +Libera espaço apagando arquivos temporários e esvaziando a lixeira — sempre mostrando antes quanto dá para recuperar. ## Como abrir -Abra o arquivo `limpeza-segura.cmd` na pasta `Úteis` (duplo clique ou Execute). +Dê um duplo-clique em `Úteis\limpeza-segura.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra ## Passo a passo -1. Aguarde a análise. Nada é apagado ainda. -2. Veja a prévia: temporários do usuário, do Windows e cache de atualizações, com tamanho aproximado e total recuperável. -3. Confirme com `s` (ou `sim`) para limpar esses temporários. Qualquer outra resposta pula essa etapa. Arquivos em uso são pulados automaticamente. -4. Em seguida, o programa pergunta se quer **esvaziar a Lixeira** também (`s`/`sim` ou não). -5. Ao terminar, veja quanto espaço foi liberado (quando a limpeza rodou). +1. O programa **pede permissão de administrador** (sem ela, os temporários do Windows não podem ser lidos nem limpos). +2. Veja a **prévia** com o tamanho de cada local. +3. Confirme se quer limpar. +4. Responda se quer esvaziar a **lixeira** também. + +## O que é limpo + +- Temporários do usuário (`%TEMP%`) +- Temporários do Windows (`C:\Windows\Temp`) +- Cache de downloads do Windows Update +- Lixeira (opcional, pergunta separada) + +## Onde salva + +Nada é salvo — só apaga. ## Dicas -- A limpeza é só de pastas temporárias/cache listadas — não mexe em Documentos, Downloads, etc. -- Esvaziar a Lixeira é permanente; confirme só se tiver certeza. -- Pressione Enter no final para fechar a janela. +- **Nada é apagado sem você confirmar.** +- Arquivos em uso são pulados automaticamente. +- De propósito **não** mexe no Prefetch: apagá-lo não traz ganho e pode deixar os programas abrindo mais devagar. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,42 @@
+# Marca d'água em Vídeo + +Coloca um texto ou um logo por cima dos vídeos, em lote. + +## Como abrir + +Dê um duplo-clique em `Vídeos\marca-dagua-video.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **FFmpeg** — `winget install Gyan.FFmpeg` +- *(opcional)* placa **NVIDIA** — é usada automaticamente se existir + +## Passo a passo + +1. Informe a **pasta ou um vídeo**. +2. Escolha **texto** ou **logo** (PNG). +3. Escolha a **posição** (5 opções). +4. Confirme a **pasta de saída**. + +## Opções + +| # | Posição | +|---|---------| +| 1 | Inferior direita | +| 2 | Inferior esquerda | +| 3 | Superior direita | +| 4 | Superior esquerda | +| 5 | Centro | + +## Onde salva + +Na pasta de saída escolhida (padrão: subpasta `ComMarca`). Sai sempre em MP4. + +## Dicas + +- O **áudio é copiado sem recodificar**, então não perde qualidade nem tempo. +- Use um PNG com fundo transparente para o logo ficar bonito. +- O tamanho do logo é em % da largura do vídeo (padrão 20%), então fica proporcional em qualquer resolução. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,32 +1,31 @@
# Marca d'água -Programa para aplicar texto ou logo em imagens, uma de cada vez ou várias de uma pasta. +Aplica um texto ou um logo sobre as imagens, em lote. ## Como abrir -Abra o arquivo `marca-dagua.cmd` na pasta `Imagens` (duplo clique ou Execute). +Dê um duplo-clique em `Imagens\marca-dagua.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa -Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo. +- **ImageMagick** — `winget install ImageMagick.ImageMagick` ## Passo a passo -1. **Pasta ou imagem** - Confirme o caminho sugerido (Enter) ou cole/digite uma pasta ou uma imagem. +1. Informe a **pasta ou uma imagem**. +2. Escolha **texto** ou **logo** (PNG). +3. Escolha a **posição** (5 opções). +4. Confirme a **pasta de saída**. -2. **Tipo de marca** - - **Texto** — digite o texto que aparecerá na foto. - - **Logo** — informe o caminho de um PNG (de preferência com fundo transparente) e, se quiser, o tamanho em % da largura (Enter = 20). -3. **Posição** - Escolha: inferior direita (padrão), inferior esquerda, superior direita, superior esquerda ou centro. - -4. **Aguarde o processamento** - Cada imagem é listada; as que já existem no destino são ignoradas. +## Onde salva -5. **Resultado** - As fotos ficam na pasta `ComMarca`, dentro da pasta de origem. Subpastas são preservadas. +Na pasta de saída escolhida (padrão: subpasta `ComMarca`), preservando as subpastas. ## Dicas -- O programa lembra a última pasta e opções usadas. -- Pressione Enter no final para fechar a janela. +- O tamanho do texto e do logo é **proporcional à imagem**, então fica igual em fotos grandes e pequenas. +- Use PNG com fundo transparente para o logo. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,37 @@
+# Normalizar Áudio + +Deixa todos os arquivos com o mesmo volume percebido, usando o padrão EBU R128 — acaba com aquele vídeo baixo demais e o seguinte estourando. + +## Como abrir + +Dê um duplo-clique em `Vídeos\normalizar-audio.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- **FFmpeg** — `winget install Gyan.FFmpeg` + +## Passo a passo + +1. Informe a **pasta ou um arquivo** (vídeo ou áudio). +2. Escolha o **volume alvo**. +3. Confirme a **pasta de saída**. + +## Opções + +| # | Alvo | Para que serve | +|---|------|----------------| +| 1 | -14 LUFS | Streaming / YouTube | +| 2 | -16 LUFS | Podcast / voz | +| 3 | -23 LUFS | TV / broadcast | + +## Onde salva + +Na pasta de saída escolhida (padrão: subpasta `Normalizado`). + +## Dicas + +- Em vídeos, a **imagem é copiada sem recodificar** — só o áudio é tratado. +- Processa vários ao mesmo tempo. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,20 +1,33 @@
-# Organizar por tipo +# Organizar por Tipo -Programa que separa os arquivos soltos de uma pasta em subpastas por categoria (imagens, vídeos, áudio, documentos, etc.). +Pega uma pasta bagunçada e separa os arquivos em subpastas por categoria. ## Como abrir -Abra o arquivo `organizar-por-tipo.cmd` na pasta `Organizar` (duplo clique ou Execute). +Dê um duplo-clique em `Organizar\organizar-por-tipo.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra ## Passo a passo -1. Cole o caminho da pasta a organizar. -2. Veja a prévia: quantos arquivos vão para cada categoria (Imagens, Vídeos, Áudio, Documentos, Compactados, Programas, Outros). -3. Confirme com `s` (ou `sim`) para mover. Qualquer outra resposta cancela. -4. Os arquivos são movidos para subpastas com esses nomes, **dentro da mesma pasta**. Se já existir um arquivo com o mesmo nome, um número é acrescentado. +1. Informe a **pasta**. +2. Veja a **prévia** com a contagem por categoria. +3. Confirme. + +## Categorias + +Imagens · Vídeos · Áudio · Documentos · Compactados · Programas · Outros + +## Onde salva + +Cria as subpastas dentro da própria pasta e move os arquivos para elas. ## Dicas -- Só organiza arquivos **soltos** na pasta (não entra em subpastas já existentes). -- Confira a prévia antes de confirmar. -- Pressione Enter no final para fechar a janela. +- Só mexe nos arquivos **soltos no primeiro nível** — não entra nas subpastas que já existem. +- O que não se encaixa em nenhuma categoria vai para `Outros`. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,19 +1,29 @@
-# Relatório do sistema +# Relatório do Sistema -Programa que mostra um resumo do computador: nome, sistema, processador, memória, vídeo, discos e espaço nas unidades. +Um retrato do computador: processador, memória, placa de vídeo, discos e espaço livre em cada unidade. ## Como abrir -Abra o arquivo `relatorio-sistema.cmd` na pasta `Úteis` (duplo clique ou Execute). +Dê um duplo-clique em `Úteis\relatorio-sistema.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra ## Passo a passo -1. O relatório aparece na tela em seções (computador, processador e memória, vídeo, discos, espaço nas unidades). -2. No final, escolha se quer **salvar num arquivo .txt** (`s`/`sim` ou não). - Se salvar, o arquivo vai para a **Área de trabalho**, com data e hora no nome. -3. Pressione Enter para fechar. +1. Abra — o relatório aparece na hora. +2. Escolha se quer **salvar num arquivo .txt**. + + +## Onde salva + +Se você pedir, salva um `relatorio-sistema_<data>.txt` na Área de Trabalho. ## Dicas -- Útil para guardar um “retrato” do PC ou enviar para alguém que vá te ajudar. -- Unidades quase cheias aparecem em destaque na tela. +- Útil para mandar para o suporte ou guardar a configuração da máquina. +- As unidades com mais de 90% de uso aparecem em vermelho. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,23 +1,40 @@
-# Renomear em lote +# Renomear em Lote -Programa para renomear vários arquivos de uma pasta de uma vez, ou organizá-los em pastas por data. +Renomeia vários arquivos de uma vez, de quatro formas diferentes — sempre com prévia antes de mexer em qualquer coisa. ## Como abrir -Abra o arquivo `renomear-em-lote.cmd` na pasta `Organizar` (duplo clique ou Execute). +Dê um duplo-clique em `Organizar\renomear-em-lote.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra (o **ImageMagick** é usado, se existir, para ler a data da foto) ## Passo a passo -1. Cole o caminho da pasta (só arquivos **nessa pasta**, sem subpastas). -2. Escolha o modo: - - **Sequência** — prefixo + número (ex.: Foto_001). Informe prefixo, número inicial e quantos dígitos. - - **Data/hora** — nome com data e hora do arquivo (em fotos, tenta usar a data da foto quando disponível). - - **Localizar e substituir** — troca um trecho do nome por outro (pode deixar o substituto vazio para remover). - - **Mover para pastas por data** — cria pastas `AAAA-MM` e move os arquivos conforme a data. -3. Veja a prévia na tela e confirme com `s` (ou `sim`). Qualquer outra resposta cancela. +1. Informe a **pasta**. +2. Escolha o **modo**. +3. Responda o que o modo pedir. +4. **Confira a prévia** e confirme. + +## Opções + +| # | Modo | Resultado | +|---|------|-----------| +| 1 | Sequência | `Foto_001`, `Foto_002`, ... | +| 2 | Data/hora | `2026-09-19_14-30-05` | +| 3 | Localizar e substituir | Troca um texto no nome | +| 4 | Mover para pastas por data | Cria pastas `2026-09`, `2026-08`... | + +## Onde salva + +Renomeia na própria pasta. No modo 4, move os arquivos para subpastas por mês. ## Dicas -- Sempre há prévia antes de aplicar — confira antes de confirmar. -- Se o novo nome já existir, o programa evita conflito acrescentando um número. -- Pressione Enter no final para fechar a janela. +- Nos modos 2 e 4 ele tenta ler a **data original da foto** (EXIF); se não achar, usa a data do arquivo. +- Nomes repetidos ganham `(1)`, `(2)`... automaticamente — nada é sobrescrito. +- **Sempre mostra a prévia** e pede confirmação antes de renomear. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,39 +1,53 @@
-# Transcodificar +# Transcodificar Vídeos -Programa para converter vídeos, um de cada vez ou vários de uma pasta, com escolha de qualidade. +Converte vídeos em lote para MP4, escolhendo a qualidade e quem faz o trabalho pesado (placa de vídeo ou processador). ## Como abrir -Abra o arquivo `transcodificar.cmd` na pasta `Vídeos` (duplo clique ou Execute). +Dê um duplo-clique em `Vídeos\transcodificar.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. -Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo. +## O que precisa + +- **FFmpeg** — `winget install Gyan.FFmpeg` +- *(opcional)* placa **NVIDIA** para o modo GPU ## Passo a passo -1. **Pasta ou vídeo** - Confirme o caminho sugerido (Enter) ou cole/digite o caminho de uma pasta ou de um arquivo de vídeo. - Se a pasta não existir, o programa pergunta se quer criá-la. +1. Informe a **pasta ou um vídeo** (ele lembra o último caminho). +2. Escolha a **qualidade** (1 a 5). +3. Escolha o **motor** de codificação (velocidade × tamanho). +4. Confirme a **pasta de saída** (é criada se não existir). +5. Ele converte vários ao mesmo tempo, com barra de progresso. -2. **Qualidade de saída** - Escolha o número da opção: - - Máxima (resolução original) - - Alta (1080p) - - Média (720p) - - Baixa (480p) - - Mínima (compacta, para prévia ou envio rápido) +## Opções - Enter usa a opção marcada como padrão (a última que você usou; se for a primeira vez, costuma ser a mínima). +**Qualidade** -3. **Aguarde o processamento** - O programa lista cada vídeo e mostra se foi convertido, ignorado (já existia no destino) ou se deu erro. Vídeos longos podem demorar. +| # | Perfil | Resolução | CRF | +|---|--------|-----------|-----| +| 1 | Máxima | original | 18 | +| 2 | Alta | 1080p | 20 | +| 3 | Média | 720p | 23 | +| 4 | Baixa | 480p | 26 | +| 5 | Mínima (compacta) | ~320p | 30 | -4. **Resultado** - Os arquivos ficam na pasta `Transcodificado`, dentro da pasta de origem (ou ao lado do vídeo único), em formato adequado para uso geral. A estrutura de subpastas é preservada. +**Motor** + +| # | Motor | Quando usar | +|---|-------|-------------| +| 1 | GPU NVENC | Qualidades altas e vídeos longos — bem mais rápido | +| 2 | CPU rápido (veryfast) | Melhor para a qualidade mínima e lotes | +| 3 | CPU qualidade (medium) | Arquivo menor no mesmo nível | + +## Onde salva + +Na pasta de saída que você escolher (padrão: subpasta `Transcodificado` dentro da origem). A estrutura de subpastas é preservada. ## Dicas -- Você pode processar uma pasta inteira (incluindo subpastas) ou só um arquivo. -- Formatos comuns de vídeo são aceitos. -- Qualidade maior = arquivo maior e conversão mais lenta; qualidade menor = arquivo mais leve. -- O programa lembra a pasta e a qualidade usadas nas últimas execuções. -- Pressione Enter no final para fechar a janela. +- Nunca amplia o vídeo: se a origem for menor que o perfil, mantém o tamanho. +- Arquivos já convertidos são **pulados** — dá para rodar de novo sem medo. +- Para a qualidade mínima, o **CPU rápido** costuma bater a GPU (o vídeo é pequeno demais para compensar ligar a placa). + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -1,26 +1,39 @@
# Verificar Discos -Programa para consultar a saúde dos HDs e SSDs do computador. Só lê informações; não apaga nem altera dados. +Mostra a saúde dos seus HDs e SSDs — temperatura, desgaste, erros — e pode fazer um teste completo de leitura procurando setores defeituosos. ## Como abrir -Abra o arquivo `verificar-discos.cmd` na pasta `Úteis` (duplo clique ou Execute). +Dê um duplo-clique em `Úteis\verificar-discos.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. -O Windows pode pedir permissão de administrador — aceite para ver o máximo de informações. Sem isso, parte dos dados pode faltar. +## O que precisa -## Menu +- Nenhum programa extra (usa o próprio Windows) +- *(opcional)* **smartmontools** — `winget install smartmontools` — para os detalhes SMART avançados -Depois de abrir, escolha uma opção pelo número: +## Passo a passo + +1. O programa **pede permissão de administrador** (necessária para ler os sensores). +2. Escolha uma opção do menu. +3. Ele volta ao menu ao terminar. + +## Opções + +| # | Opção | Tempo | +|---|-------|-------| +| 1 | Diagnóstico de saúde | Segundos | +| 2 | Teste de superfície | **Horas** — lê o disco inteiro | +| 3 | Resumo simples | Instantâneo | -1. **Diagnóstico de saúde** — rápido e recomendado no dia a dia. Mostra, para cada disco, se está bem, temperatura, desgaste, horas ligado e erros, quando disponíveis. -2. **Teste de superfície** — lê o disco inteiro em busca de problemas. Pode levar horas. É seguro (só leitura). Você escolhe qual disco testar; Enter cancela. Use Ctrl+C se quiser interromper no meio. -3. **Resumo simples** — lista rápida de cada disco com OK ou aviso/falha. -0. **Sair** — fecha o programa (Enter vazio também sai). +## Onde salva -Após as opções 1, 2 ou 3, pressione Enter para voltar ao menu. +Nada é salvo — é só leitura. ## Dicas -- Comece pelo diagnóstico de saúde (opção 1); use o teste de superfície só se quiser uma verificação mais profunda. -- O teste de superfície demora muito em discos grandes — deixe o PC ligado e evite desligar no meio. -- Se algum disco aparecer com aviso ou falha, considere fazer backup e procurar assistência. +- **Nada é alterado no disco**: mesmo o teste de superfície apenas *lê*. +- HDs externos por USB às vezes não expõem os dados SMART — é limitação da gaveta, não do programa. +- O teste de superfície pode ser cancelado a qualquer momento com Ctrl+C. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,39 @@
+# Verificar Integridade + +Gera uma "impressão digital" de cada arquivo de uma pasta e, depois, confere se algo mudou — detectando corrupção silenciosa e cópias que deram errado. + +## Como abrir + +Dê um duplo-clique em `Úteis\verificar-integridade.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número. + +## O que precisa + +- Nenhum programa extra + +## Passo a passo + +1. Escolha **[1] Gerar** o registro ou **[2] Conferir**. +2. Informe a **pasta**. +3. Aguarde o cálculo (mostra o progresso). + +## O resultado da conferência + +| Estado | Significa | +|--------|-----------| +| Intactos | O conteúdo continua igual | +| **ALTERADOS** | O conteúdo mudou — possível corrupção | +| Sumidos | Estavam no registro e não existem mais | +| Novos | Apareceram depois do registro | + +## Onde salva + +Um arquivo `integridade.sha256` dentro da própria pasta analisada. + +## Dicas + +- Rode o **[1] Gerar** logo depois de fazer um backup, e o **[2] Conferir** meses depois para saber se algum arquivo apodreceu no disco. +- Combina com o [backup-espelhado](backup-espelhado.md): copie e depois confira se chegou íntegro. +- Nada é alterado nos seus arquivos — só leitura. + +--- +*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,175 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Analisar Espaço - mostra o que está ocupando o disco +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Analisar Espaço' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'AnalisarEspaco' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Analisar Espaço' +Write-Host '[OK] Não precisa de nenhum programa extra. Só leitura: nada é alterado.' -ForegroundColor Green +$config = Carregar-Config + +Titulo 'Espaço nas unidades' +Get-Volume | Where-Object { $_.DriveLetter -and $_.Size -gt 0 } | Sort-Object DriveLetter | ForEach-Object { + $usado = $_.Size - $_.SizeRemaining + $pc = if ($_.Size -gt 0) { [int](($usado / $_.Size) * 100) } else { 0 } + $ch = [int]($pc / 5) + $cor = if ($pc -ge 90) { 'Red' } elseif ($pc -ge 75) { 'Yellow' } else { 'Gray' } + Write-Host (" {0}: [{1}] {2,3}% {3} livres de {4}" -f $_.DriveLetter, (('#'*$ch)+('-'*(20-$ch))), $pc, (Tam $_.SizeRemaining), (Tam $_.Size)) -ForegroundColor $cor +} + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' } +$pasta = $null +while (-not $pasta) { + Titulo 'Pasta para analisar' + Write-Host " $padrao" -ForegroundColor White + $r = Read-Host 'Pasta (Enter = confirmar)' + $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x } +} +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta }) + +Titulo 'Analisando' +Write-Host 'Lendo a árvore de pastas (pode demorar em pastas enormes)...' -ForegroundColor Gray +$todos = @(Get-ChildItem -LiteralPath $pasta -Recurse -File -Force -ErrorAction SilentlyContinue) +if ($todos.Count -eq 0) { Write-Host 'Pasta vazia ou sem acesso.' -ForegroundColor Yellow; Pausar; exit 0 } +$total = ($todos | Measure-Object -Property Length -Sum).Sum + +Titulo 'Maiores subpastas' +$subs = @(Get-ChildItem -LiteralPath $pasta -Directory -Force -ErrorAction SilentlyContinue) +$linhas = @() +foreach ($s in $subs) { + $b = (Get-ChildItem -LiteralPath $s.FullName -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum + $linhas += [pscustomobject]@{ Nome=$s.Name; Bytes=[int64]$b } +} +$naRaiz = ($todos | Where-Object { $_.DirectoryName -eq $pasta } | Measure-Object -Property Length -Sum).Sum +if ($naRaiz -gt 0) { $linhas += [pscustomobject]@{ Nome='(arquivos soltos aqui)'; Bytes=[int64]$naRaiz } } +$linhas = $linhas | Sort-Object Bytes -Descending | Select-Object -First 15 +foreach ($l in $linhas) { + $pc = if ($total -gt 0) { [int](($l.Bytes / $total) * 100) } else { 0 } + $ch = [int]($pc / 5) + Write-Host (" {0,10} [{1}] {2,3}% {3}" -f (Tam $l.Bytes), (('#'*$ch)+('-'*(20-$ch))), $pc, $l.Nome) -ForegroundColor Gray +} + +Titulo 'Maiores arquivos' +$todos | Sort-Object Length -Descending | Select-Object -First 15 | ForEach-Object { + Write-Host (" {0,10} {1}" -f (Tam $_.Length), $_.FullName.Substring($pasta.Length).TrimStart('\')) -ForegroundColor Gray +} + +Titulo 'Resumo' +Write-Host ("Pasta: {0}" -f $pasta) +Write-Host ("Arquivos: {0:N0}" -f $todos.Count) +Write-Host ("Tamanho: {0}" -f (Tam $total)) -ForegroundColor Cyan +Pausar
@@ -0,0 +1,194 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Backup Espelhado - copia/espelha pastas com robocopy multithread +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Backup Espelhado' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'BackupEspelhado' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Backup Espelhado' +Write-Host '[OK] Usa o robocopy do próprio Windows (multithread).' -ForegroundColor Green +$config = Carregar-Config + +$origem = $null +$padrao = if ($config -and $config.UltimaOrigem) { $config.UltimaOrigem } else { Join-Path $env:USERPROFILE 'Documents' } +while (-not $origem) { + Titulo 'Pasta de ORIGEM (o que será copiado)' + Write-Host " $padrao" -ForegroundColor White + $r = Read-Host 'Origem (Enter = confirmar)' + $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $x -PathType Container) { $origem = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x } +} +$destino = Pedir-Saida $(if ($config -and $config.UltimoDestino) { $config.UltimoDestino } else { 'D:\Backup' }) +if ($destino.TrimEnd('\') -ieq $origem.TrimEnd('\')) { Write-Host '[ERRO] Origem e destino são a mesma pasta.' -ForegroundColor Red; Pausar; exit 1 } + +Titulo 'Modo' +Write-Host '> [1] Cópia - só adiciona/atualiza; NUNCA apaga nada no destino' -ForegroundColor White +Write-Host ' [2] Espelho - deixa o destino IDÊNTICO à origem (APAGA no destino o que não existe mais na origem)' -ForegroundColor Yellow +$m = Read-Host 'Modo (Enter = 1)' +$espelho = ($m.Trim() -eq '2') +$modoArg = if ($espelho) { '/MIR' } else { '/E' } + +Salvar-Config ([pscustomobject]@{ UltimaOrigem=$origem; UltimoDestino=$destino }) + +$comuns = @($origem, $destino, '/MT:16', '/R:1', '/W:1', '/NP', '/NDL', '/XJ') + +Titulo 'Prévia (simulação - nada é copiado ou apagado)' +Write-Host 'Analisando as diferenças...' -ForegroundColor Gray +$previa = & robocopy @comuns $modoArg /L /NJH /NJS 2>&1 +$aCopiar = @($previa | Where-Object { $_ -match '\S' -and $_ -notmatch '^\s*\*.*EXTRA' }).Count +$aApagar = @($previa | Where-Object { $_ -match '^\s*\*.*EXTRA' }).Count +Write-Host '' +Write-Host (" Serão copiados/atualizados: {0} item(ns)" -f $aCopiar) -ForegroundColor Cyan +if ($espelho) { + Write-Host (" Serão APAGADOS no destino: {0} item(ns)" -f $aApagar) -ForegroundColor $(if ($aApagar -gt 0) { 'Red' } else { 'Gray' }) + if ($aApagar -gt 0) { + Write-Host '' + Write-Host ' Exemplos do que será apagado no destino:' -ForegroundColor Red + $previa | Where-Object { $_ -match '^\s*\*.*EXTRA' } | Select-Object -First 10 | ForEach-Object { Write-Host (" " + $_.Trim()) -ForegroundColor DarkGray } + } +} +Write-Host '' +Write-Host "Origem: $origem" +Write-Host "Destino: $destino" +Write-Host ("Modo: {0}" -f $(if ($espelho) { 'ESPELHO (apaga no destino)' } else { 'Cópia (não apaga)' })) -ForegroundColor $(if ($espelho) { 'Yellow' } else { 'Gray' }) +Write-Host '' +if ($espelho -and $aApagar -gt 0) { + $conf = Read-Host 'Isto APAGA arquivos no destino. Digite ESPELHAR para confirmar' + if ($conf -cne 'ESPELHAR') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 } +} else { + $conf = Read-Host 'Fazer o backup agora? (s/N)' + if ($conf -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 } +} + +$pastaLog = Join-Path $env:LOCALAPPDATA 'fast-scripts\logs-backup' +New-Item -ItemType Directory -Path $pastaLog -Force | Out-Null +$log = Join-Path $pastaLog ("backup_{0}.log" -f (Get-Date -Format 'yyyy-MM-dd_HH-mm')) +Titulo 'Copiando' +Write-Host "Log: $log" -ForegroundColor DarkGray +Write-Host '' +$t = Measure-Command { & robocopy @comuns $modoArg "/LOG+:$log" /TEE | Out-Null } +$rc = $LASTEXITCODE + +# robocopy: 0-7 = sucesso (8+ = erro real) +Titulo 'Concluído' +if ($rc -lt 8) { + Write-Host ("[OK] Backup finalizado em {0:hh\:mm\:ss}." -f $t) -ForegroundColor Green + Write-Host ("Código do robocopy: {0} (0-7 = sucesso)" -f $rc) -ForegroundColor DarkGray +} else { + Write-Host ("[ERRO] O robocopy retornou {0} - houve falhas. Veja o log." -f $rc) -ForegroundColor Red +} +Write-Host "Destino: $destino" +Write-Host "Log: $log" +Pausar
@@ -1,6 +1,7 @@
<# : @echo off -powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +set "SELF=%~f0" +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText($env:SELF,[System.Text.Encoding]::UTF8))" exit /b %errorlevel% #>@@ -13,6 +14,17 @@
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} try { $Host.UI.RawUI.WindowTitle = 'Limpeza segura' } catch {} +# ------------------------------------------------------------ +# Auto-elevacao: sem admin, C:\Windows\Temp e o cache do Windows +# Update nao podem ser lidos nem limpos (falhava em silencio). +# ------------------------------------------------------------ +$souAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $souAdmin) { + Write-Host 'Solicitando privilegios de administrador...' -ForegroundColor Yellow + try { Start-Process -FilePath 'cmd.exe' -ArgumentList '/c', "`"$env:SELF`"" -Verb RunAs | Out-Null } + catch { Write-Host '[!] Sem elevacao: os temporarios do Windows serao ignorados.' -ForegroundColor Yellow; Start-Sleep -Seconds 2 } + exit +} function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } function Titulo($t) { Write-Host ''
@@ -0,0 +1,231 @@
+<# : +@echo off +powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))" +exit /b %errorlevel% +#> + +# ============================================================ +# Verificar Integridade - detecta arquivos corrompidos por checksum SHA256 +# Desenvolvido por Pablo Murad - 2026 +# ============================================================ + +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} +try { $Host.UI.RawUI.WindowTitle = 'Verificar Integridade' } catch {} +function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() } +function Titulo($t) { + Write-Host '' + Write-Host '============================================================' -ForegroundColor DarkCyan + Write-Host " $t" -ForegroundColor Cyan + Write-Host '============================================================' -ForegroundColor DarkCyan +} +function Tam($bytes) { + if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) } + elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) } + else { '{0:N0} KB' -f ($bytes/1KB) } +} + +$configDir = Join-Path $env:APPDATA 'VerificarIntegridade' +$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($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} } + +# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir +function Pedir-Saida($padrao) { + while ($true) { + Titulo 'Pasta de saída' + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = usar esta pasta | ou cole/digite outra' -ForegroundColor DarkGray + $r = Read-Host 'Pasta de saída' + $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path } + catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow } + } +} + +function Checar($exe, $nomeAmigavel, $comandoInstalar) { + $c = Get-Command $exe -ErrorAction SilentlyContinue + if (-not $c) { + Write-Host '' + Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red + Write-Host "Instale com: $comandoInstalar" -ForegroundColor White + Pausar; exit 1 + } + Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green + return $c +} + +function Abrir($titulo) { + Titulo $titulo + Write-Host ' Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray + Write-Host '' + Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray +} + +$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob') +$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus') +$extImg = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff') + +# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem } +function Pedir-Entrada($padrao, $exts, $rotulo) { + while ($true) { + Titulo $rotulo + Write-Host " $padrao" -ForegroundColor White + Write-Host 'Enter = confirmar | ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray + $r = Read-Host 'Caminho' + $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $c -PathType Leaf) { + if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) { + Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue + } + $f = Get-Item -LiteralPath $c + return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName } + } + elseif (Test-Path -LiteralPath $c -PathType Container) { + $p = (Resolve-Path -LiteralPath $c).Path + return @{ Modo='pasta'; Caminho=$p; Origem=$p } + } + Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow + $padrao = $c + } +} + +function Listar($ent, $exts) { + if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) } + return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name) +} + +function Resumo($ok, $ign, $err, $destino) { + Titulo 'Concluído' + Write-Host "Prontos: $ok" -ForegroundColor Green + if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow } + Write-Host "Erros: $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' }) + if ($destino) { Write-Host "Destino: $destino" } + Pausar +} + +function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) { + foreach ($x in $resultados) { + $alvo = $tarefas[$x.Indice].Alvo + if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ } + else { + $err.Value++ + Write-Host (" [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red + $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1) + if ($motivo) { Write-Host (" {0}" -f $motivo.Trim()) -ForegroundColor DarkGray } + if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue } + } + } +} + +Abrir 'Verificar Integridade' +Write-Host '[OK] Não precisa de nenhum programa extra.' -ForegroundColor Green +$config = Carregar-Config + +Write-Host '' +Write-Host 'Gera uma "impressão digital" de cada arquivo e confere depois,' -ForegroundColor DarkGray +Write-Host 'detectando arquivos corrompidos silenciosamente ou cópias com erro.' -ForegroundColor DarkGray + +Titulo 'O que você quer fazer?' +Write-Host ' [1] Gerar o registro de integridade da pasta' -ForegroundColor White +Write-Host ' [2] Conferir a pasta contra um registro existente' -ForegroundColor White +$acao = $null +while (-not $acao) { $a = Read-Host 'Opção (1-2)'; switch ($a.Trim()) { '1' { $acao='gerar' } '2' { $acao='conferir' } default { Write-Host '[!] Digite 1 ou 2.' -ForegroundColor Yellow } } } + +$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Documents' } +$pasta = $null +while (-not $pasta) { + Titulo 'Pasta' + Write-Host " $padrao" -ForegroundColor White + $r = Read-Host 'Pasta (Enter = confirmar)' + $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') } + if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x } +} +Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta }) +$registro = Join-Path $pasta 'integridade.sha256' + +$arquivos = @(Get-ChildItem -LiteralPath $pasta -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'integridade.sha256' }) +if ($arquivos.Count -eq 0) { Write-Host 'Nenhum arquivo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 } + +function Hash-SHA256([string]$caminho) { + $sha = [System.Security.Cryptography.SHA256]::Create() + $fs = [System.IO.File]::OpenRead($caminho) + try { return ([BitConverter]::ToString($sha.ComputeHash($fs)) -replace '-', '') } + finally { $fs.Close(); $sha.Dispose() } +} +# $base e' passado explicitamente: dentro da funcao o escopo do script +# nao e' confiavel quando o arquivo roda via Invoke-Expression. +function Calcular($lista, $rotulo, $base) { + $mapa = @{} + $i = 0 + $falhas = 0 + foreach ($f in $lista) { + $i++ + $pc = [int](($i / $lista.Count) * 100); $ch = [int]($pc / 5) + Write-Host ("`r {0} [{1}] {2,3}% {3}/{4} " -f $rotulo, (('#'*$ch)+('-'*(20-$ch))), $pc, $i, $lista.Count) -NoNewline -ForegroundColor Cyan + $rel = $f.FullName + if ($base -and $f.FullName.StartsWith($base, [System.StringComparison]::OrdinalIgnoreCase)) { + $rel = $f.FullName.Substring($base.Length).TrimStart('\') + } + try { $mapa[$rel] = Hash-SHA256 $f.FullName } + catch { + $falhas++ + Write-Host ("`r [ERRO] {0}: {1}" -f $f.Name, $_.Exception.Message) -ForegroundColor Red + } + } + Write-Host '' + if ($falhas -gt 0) { Write-Host (" [!] {0} arquivo(s) nao puderam ser lidos." -f $falhas) -ForegroundColor Yellow } + return $mapa +} + +if ($acao -eq 'gerar') { + Titulo 'Gerando' + $mapa = Calcular $arquivos 'Calculando' $pasta + $sb = New-Object System.Text.StringBuilder + foreach ($k in ($mapa.Keys | Sort-Object)) { [void]$sb.AppendLine("$($mapa[$k]) *$k") } + [System.IO.File]::WriteAllText($registro, $sb.ToString(), (New-Object System.Text.UTF8Encoding($false))) + Titulo 'Concluído' + Write-Host ("[OK] Registro criado com {0} arquivo(s)." -f $mapa.Count) -ForegroundColor Green + Write-Host "Arquivo: $registro" + Write-Host 'Guarde-o junto com os dados: rode a opção [2] depois para conferir.' -ForegroundColor DarkGray + Pausar +} +else { + if (-not (Test-Path -LiteralPath $registro)) { + Write-Host '' + Write-Host "[ERRO] Não achei o registro: $registro" -ForegroundColor Red + Write-Host 'Rode primeiro a opção [1] nessa pasta.' -ForegroundColor Yellow + Pausar; exit 1 + } + $antigo = @{} + foreach ($l in [System.IO.File]::ReadAllLines($registro, [System.Text.Encoding]::UTF8)) { + $m = [regex]::Match($l, '^([0-9A-Fa-f]{64}) \*(.+)$') + if ($m.Success) { $antigo[$m.Groups[2].Value] = $m.Groups[1].Value.ToUpperInvariant() } + } + Titulo 'Conferindo' + $novo = Calcular $arquivos 'Conferindo' $pasta + + $iguais=0; $alterados=@(); $sumidos=@(); $novos=@() + foreach ($k in $antigo.Keys) { + if (-not $novo.ContainsKey($k)) { $sumidos += $k } + elseif ($novo[$k] -ne $antigo[$k]) { $alterados += $k } + else { $iguais++ } + } + foreach ($k in $novo.Keys) { if (-not $antigo.ContainsKey($k)) { $novos += $k } } + + Titulo 'Resultado' + Write-Host (" Intactos: {0}" -f $iguais) -ForegroundColor Green + Write-Host (" ALTERADOS: {0}" -f $alterados.Count) -ForegroundColor $(if ($alterados.Count) { 'Red' } else { 'Gray' }) + Write-Host (" Sumidos: {0}" -f $sumidos.Count) -ForegroundColor $(if ($sumidos.Count) { 'Yellow' } else { 'Gray' }) + Write-Host (" Novos: {0}" -f $novos.Count) -ForegroundColor Gray + if ($alterados.Count) { + Write-Host '' + Write-Host ' Arquivos com conteúdo diferente (possível corrupção):' -ForegroundColor Red + $alterados | Select-Object -First 20 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } + } + if ($sumidos.Count) { + Write-Host '' + Write-Host ' Arquivos que sumiram:' -ForegroundColor Yellow + $sumidos | Select-Object -First 20 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } + } + Pausar +}