cd ..
root@paste:~/7NaNmJPzy2$cat paste
paste://7NaNmJPzy2PUBLIC
public paste

MK5_Converter_Slider_Pro_Master_v18_-_Diseño_nuevo_con_funcionamiento_original

19 views146,879 chars

stdout

Paste content

>_phpauto-detected
@echo off
setlocal EnableExtensions
title MKV Converter

REM ============================================================
REM
REM   MKV CONVERTER - VERSION GRAFICA
REM
REM   Requisitos:
REM       ffmpeg.exe
REM       ffprobe.exe
REM
REM   Ambos deben estar junto a este BAT.
REM
REM ============================================================

set "APP_DIR=%~dp0"
set "FFMPEG=%APP_DIR%ffmpeg.exe"
set "FFPROBE=%APP_DIR%ffprobe.exe"

if not exist "%FFMPEG%" (
    echo.
    echo [ERROR] No se encuentra ffmpeg.exe
    echo.
    echo Debe estar junto a este archivo BAT:
    echo.
    echo %APP_DIR%
    echo.
    pause
    exit /b 1
)

if not exist "%FFPROBE%" (
    echo.
    echo [ERROR] No se encuentra ffprobe.exe
    echo.
    echo Debe estar junto a este archivo BAT:
    echo.
    echo %APP_DIR%
    echo.
    pause
    exit /b 1
)

REM ============================================================
REM Lanzar interfaz grafica
REM ============================================================

start "" powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -WindowStyle Hidden -Command ^
"$bat='%~f0'; $lines=[System.IO.File]::ReadAllLines($bat,[System.Text.Encoding]::UTF8); $start=[Array]::IndexOf($lines,'# POWERSHELL_GUI_START'); if($start -lt 0){[System.Windows.Forms.MessageBox]::Show('No se encontro el codigo grafico.','MKV Converter'); exit 1}; $code=($lines[($start+1)..($lines.Length-1)] -join [Environment]::NewLine); Invoke-Expression $code"

exit /b 0


# POWERSHELL_GUI_START

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

[System.Windows.Forms.Application]::EnableVisualStyles()

# ============================================================
# CONFIGURACION
# ============================================================

$ErrorActionPreference = 'Stop'

$AppDir = Split-Path -Parent $bat
$FFmpeg = Join-Path $AppDir 'ffmpeg.exe'
$FFprobe = Join-Path $AppDir 'ffprobe.exe'

# Tamaño elegido por el usuario. Copy Direct compara contra este valor.
# Para NVENC se reservan siempre 50 MB de margen de seguridad.
$script:TargetSizeMB = 2000
$script:TargetSizeManualText = $null
$script:SliderExponent = 6.0
$script:TargetSizeBytes = [Int64]($script:TargetSizeMB * 1000000)
$script:LimitBytes = [Int64](($script:TargetSizeMB - 50) * 1000000)

$script:Files = New-Object System.Collections.ArrayList
$script:CurrentFileIndex = -1
$script:CurrentFileData = $null
$script:CurrentAudioTracks = @()
$script:CurrentSubtitleTracks = @()
$script:Converting = $false
$script:Reordering = $false
$script:CancelRequested = $false
$script:ShowOutputFolder = $false
$script:AllowClose = $false
$script:ActiveFfmpegProcess = $null
$script:ActiveOutputFile = $null
$script:SelectedFileCopyMode = $false
$script:UpdatingTargetSize = $false
$script:FormattingTargetSizeText = $false
$script:TargetSizeEditUnit = 'GB'

$script:FFmpegProgressSeconds = 0.0
$script:FFmpegProgressDuration = 0.0
$script:FFmpegLastError = ''
$script:FFmpegSpeed = 0.0

# ============================================================
# FUNCIONES GENERALES
# ============================================================

function Show-Error {
    param(
        [string]$Message,
        [string]$Title = 'MKV Converter'
    )

    [System.Windows.Forms.MessageBox]::Show(
        $Message,
        $Title,
        [System.Windows.Forms.MessageBoxButtons]::OK,
        [System.Windows.Forms.MessageBoxIcon]::Error
    ) | Out-Null
}

function Show-Info {
    param(
        [string]$Message,
        [string]$Title = 'MKV Converter'
    )

    [System.Windows.Forms.MessageBox]::Show(
        $Message,
        $Title,
        [System.Windows.Forms.MessageBoxButtons]::OK,
        [System.Windows.Forms.MessageBoxIcon]::Information
    ) | Out-Null
}

function Format-Bytes {
    param([Int64]$Bytes)

    if ($Bytes -ge 1TB) {
        return ('{0:N2} TB' -f ($Bytes / 1TB))
    }

    if ($Bytes -ge 1GB) {
        return ('{0:N2} GB' -f ($Bytes / 1GB))
    }

    if ($Bytes -ge 1MB) {
        return ('{0:N2} MB' -f ($Bytes / 1MB))
    }

    return ('{0:N0} KB' -f ($Bytes / 1KB))
}

function Format-Duration {
    param([double]$Seconds)

    if ($Seconds -le 0) {
        return '?'
    }

    $ts = [TimeSpan]::FromSeconds($Seconds)

    $hours = [int][math]::Floor($ts.TotalHours)
    $minutes = $ts.Minutes
    $seconds = $ts.Seconds

    if ($hours -gt 0) {
        return ('{0}h {1:00}m {2:00}s' -f $hours,$minutes,$seconds)
    }

    return ('{0}m {1:00}s' -f $minutes,$seconds)
}


function Get-TargetSizeText {
    if ($null -ne $script:TargetSizeManualText -and $script:TargetSizeManualText -ne '') {
        return $script:TargetSizeManualText
    }
    $mb = [double]$script:TargetSizeMB
    if ($mb -ge 1000000) { return ('{0:0.00} TB' -f ($mb / 1000000.0)) }
    if ($mb -ge 1000) { return ('{0:0.00} GB' -f ($mb / 1000.0)) }
    return ('{0:0} MB' -f $mb)
}

function Format-ExactTargetSizeText {
    param([Int64]$Bytes,[string]$Unit)
    $u = $Unit.ToUpperInvariant()

    if ($Bytes -lt 0) { $Bytes = 0 }

    # La unidad real de almacenamiento es el byte. Por eso la precision maxima
    # es: 9 decimales en GB, 12 en TB y 6 en MB. No existe aqui ningun tope de 1 TB.
    if ($u -eq 'TB') {
        $value = [decimal]$Bytes / [decimal]1000000000000
        $number = $value.ToString('0.############',[Globalization.CultureInfo]::InvariantCulture)
    } elseif ($u -eq 'GB') {
        $value = [decimal]$Bytes / [decimal]1000000000
        $number = $value.ToString('0.#########',[Globalization.CultureInfo]::InvariantCulture)
    } else {
        $value = [decimal]$Bytes / [decimal]1000000
        $number = $value.ToString('0.######',[Globalization.CultureInfo]::InvariantCulture)
    }

    return ('{0} {1}' -f $number,$u)
}

function Format-GroupedTargetSizeText {
    param([Int64]$Bytes,[string]$Unit)
    $u = $Unit.ToUpperInvariant()
    if ($Bytes -lt 0) { $Bytes = 0 }

    switch ($u) {
        'EB' { $value = [decimal]$Bytes / [decimal]1000000000000000000; $pattern = '0.##################' }
        'PB' { $value = [decimal]$Bytes / [decimal]1000000000000000;    $pattern = '0.###############' }
        'TB' { $value = [decimal]$Bytes / [decimal]1000000000000;       $pattern = '0.############' }
        'GB' { $value = [decimal]$Bytes / [decimal]1000000000;          $pattern = '0.#########' }
        default { $value = [decimal]$Bytes / [decimal]1000000; $u = 'MB'; $pattern = '0.######' }
    }

    $number = $value.ToString($pattern,[Globalization.CultureInfo]::InvariantCulture)
    if ($number.Contains('.')) {
        $parts = $number.Split('.')
        $whole = $parts[0]
        $fraction = $parts[1]
        $groups = New-Object System.Collections.Generic.List[string]
        for ($i = 0; $i -lt $fraction.Length; $i += 3) {
            $take = [math]::Min(3,$fraction.Length - $i)
            [void]$groups.Add($fraction.Substring($i,$take))
        }
        $number = $whole + ',' + ($groups -join ',')
    }

    return ('{0} {1}' -f $number,$u)
}

function Get-TargetSizeInputValue {
    $bytes = [Int64]$script:TargetSizeBytes
    if ($bytes -ge [Int64]1000000000000) { return (Format-ExactTargetSizeText $bytes 'TB').Split(' ')[0] }
    return (Format-ExactTargetSizeText $bytes 'GB').Split(' ')[0]
}

# ============================================================
# FUNCION MATEMATICA DEL SLIDER
#
# x = posicion normalizada del slider (0..1)
# y = tamaño normalizado (0..1)
#
# Puntos de diseño:
#   0%   = 0 GB
#   30%  = 4 GB
#   60%  = 15 GB
#   80%  = 40 GB
#   100% = 1000 GB (1 TB)
#
# La curva usada es:
#   y = x^1.128758 / (x^1.128758 + 90.8584*(1-x)^0.983650)
#
# El slider sigue directamente la curva, sin pasos artificiales por encima de 2 GB.
# ============================================================

$script:SliderCurveExponentA = 1.128758
$script:SliderCurveExponentB = 0.983650
$script:SliderCurveCoefficient = 90.8584

function Convert-SliderRatioToTargetMB {
    param([double]$Ratio)

    if ([double]::IsNaN($Ratio) -or [double]::IsInfinity($Ratio)) {
        $Ratio = 0.0
    }

    if ($Ratio -lt 0.0) { $Ratio = 0.0 }
    if ($Ratio -gt 1.0) { $Ratio = 1.0 }

    if ($Ratio -le 0.0) { return 0.0 }
    if ($Ratio -ge 1.0) { return 1000000.0 }

    $xA = [math]::Pow($Ratio, $script:SliderCurveExponentA)
    $oneMinusX = 1.0 - $Ratio
    $oneMinusXB = [math]::Pow($oneMinusX, $script:SliderCurveExponentB)
    $denominator = $xA + ($script:SliderCurveCoefficient * $oneMinusXB)

    if ($denominator -le 0.0) { return 0.0 }

    return (1000000.0 * ($xA / $denominator))
}

function Convert-TargetMBToSliderRatio {
    param([double]$TargetMB)

    if ([double]::IsNaN($TargetMB) -or [double]::IsInfinity($TargetMB)) {
        return 0.0
    }

    if ($TargetMB -le 0.0) { return 0.0 }
    if ($TargetMB -ge 1000000.0) { return 1.0 }

    $target = $TargetMB / 1000000.0
    $low = 0.0
    $high = 1.0

    # La inversa no tiene una expresion elemental sencilla para esta curva.
    # Una busqueda binaria de alta precision mantiene slider y tamaño sincronizados.
    for ($i = 0; $i -lt 60; $i++) {
        $mid = ($low + $high) / 2.0
        $value = Convert-SliderRatioToTargetMB $mid
        $valueRatio = $value / 1000000.0

        if ($valueRatio -lt $target) {
            $low = $mid
        } else {
            $high = $mid
        }
    }

    return (($low + $high) / 2.0)
}

function Get-SliderValueFromTargetMB {
    param([double]$TargetMB)

    if ($null -eq $trkTargetSize) { return 0 }

    $ratio = Convert-TargetMBToSliderRatio $TargetMB
    $sliderMin = [double]$trkTargetSize.Minimum
    $sliderMax = [double]$trkTargetSize.Maximum
    $sliderSpan = $sliderMax - $sliderMin

    if ($sliderSpan -le 0.0) { return [int]$sliderMin }

    $value = [int][math]::Round(
        $sliderMin + ($ratio * $sliderSpan),
        0,
        [MidpointRounding]::AwayFromZero
    )

    if ($value -lt $trkTargetSize.Minimum) { $value = $trkTargetSize.Minimum }
    if ($value -gt $trkTargetSize.Maximum) { $value = $trkTargetSize.Maximum }
    return $value
}

function Set-TargetSizeMB {
    param(
        [decimal]$RequestedMB,
        [switch]$PreserveExact
    )
    if ($RequestedMB -is [double] -and ([double]::IsNaN([double]$RequestedMB) -or [double]::IsInfinity([double]$RequestedMB))) { return $false }

    # La entrada manual no tiene tope de 1 TB. Solo queda limitada por la
    # capacidad real del valor Int64 expresado en bytes.
    $exactMB = [decimal]$RequestedMB
    if ($exactMB -lt 0) { $exactMB = 0 }

    $maxMB = [decimal][Int64]::MaxValue / [decimal]1000000
    if ($exactMB -gt $maxMB) { $exactMB = $maxMB }

    $byteDecimal = $exactMB * [decimal]1000000
    $roundedBytes = [decimal]::Round($byteDecimal,0,[MidpointRounding]::AwayFromZero)
    if ($roundedBytes -lt 0) { $roundedBytes = 0 }
    if ($roundedBytes -gt [decimal][Int64]::MaxValue) { $roundedBytes = [decimal][Int64]::MaxValue }

    $script:TargetSizeBytes = [Int64]$roundedBytes
    $script:TargetSizeMB = [double]([decimal]$script:TargetSizeBytes / [decimal]1000000)
    if (-not $PreserveExact) { $script:TargetSizeManualText = $null }

    # LimitBytes se mantiene en Int64 y usa exactamente el tamaño real menos 50 MB.
    $effectiveBytes = $script:TargetSizeBytes - [Int64]50000000
    if ($effectiveBytes -lt 0) { $effectiveBytes = 0 }
    $script:LimitBytes = $effectiveBytes

    if ($null -ne $trkTargetSize) {
        $sliderValue = Get-SliderValueFromTargetMB $script:TargetSizeMB
        if ($trkTargetSize.Value -ne $sliderValue) {
            $oldUpdating = $script:UpdatingTargetSize
            $script:UpdatingTargetSize = $true
            try { $trkTargetSize.Value = $sliderValue } finally { $script:UpdatingTargetSize = $oldUpdating }
        }
    }
    if ($null -ne $lblTargetSizeValue) { $lblTargetSizeValue.Text = Get-TargetSizeText }
    if ($null -ne $txtTargetSizeEdit -and -not $txtTargetSizeEdit.Visible) { $txtTargetSizeEdit.Text = Get-TargetSizeInputValue }

    if ($null -ne $script:CurrentFileData) {
        # Copy Direct usa el tamaño mostrado por el slider; NO resta 50 MB.
        $script:SelectedFileCopyMode = ([Int64]$script:CurrentFileData.SizeBytes -le [Int64]$script:TargetSizeBytes)
        Update-ModeDisplay
        Update-AudioDetails
    }
    return $true
}

function Get-EffectiveLimitText {
    $effectiveMB = [math]::Max(0.0, [double]$script:TargetSizeMB - 50.0)
    if ($effectiveMB -ge 1000000) { return ('{0:0.00} TB' -f ($effectiveMB / 1000000.0)) }
    if ($effectiveMB -ge 1000) { return ('{0:0.00} GB' -f ($effectiveMB / 1000.0)) }
    return ('{0:N0} MB' -f $effectiveMB)
}

function Parse-TargetSizeInput {
    param(
        [string]$Text,
        [string]$DefaultUnit = 'GB'
    )
    $clean = $Text.Trim().ToLowerInvariant()
    if ([string]::IsNullOrWhiteSpace($clean)) { return $null }

    $unit = $DefaultUnit.ToUpperInvariant()
    if ($clean -match '(eb|pb|tb|gb|mb)\s*$') {
        $unit = $Matches[1].ToUpperInvariant()
        $clean = $clean.Substring(0,$clean.Length - 2).Trim()
    }

    $clean = $clean -replace '\s+', ''
    if ($clean -notmatch '^([+-]?\d+)(?:[,.](.*))?$') { return $null }

    $whole = $Matches[1]
    $fraction = [string]$Matches[2]
    if ($Matches[2] -ne $null) {
        # La primera coma/punto es el separador decimal; cualquier coma/punto
        # posterior forma parte del agrupado de los decimales.
        $fraction = $fraction -replace '[,.]', ''
        $clean = $whole + '.' + $fraction
    } else {
        $clean = $whole
    }

    $value = [decimal]0
    if (-not [decimal]::TryParse($clean,[Globalization.NumberStyles]::Number,[Globalization.CultureInfo]::InvariantCulture,[ref]$value)) { return $null }
    if ($value -lt 0) { $value = 0 }

    switch ($unit) {
        'EB' { $multiplier = [decimal]1000000000000000000 }
        'PB' { $multiplier = [decimal]1000000000000000 }
        'TB' { $multiplier = [decimal]1000000000000 }
        'GB' { $multiplier = [decimal]1000000000 }
        default { $unit = 'MB'; $multiplier = [decimal]1000000 }
    }

    return [PSCustomObject]@{
        MB   = ($value * $multiplier / [decimal]1000000)
        Unit = $unit
    }
}

function Get-SafeOutputPath {
    param(
        [string]$Directory,
        [string]$BaseName
    )

    $candidate = Join-Path $Directory ($BaseName + '.mp4')

    $n = 1

    while (Test-Path -LiteralPath $candidate) {
        $candidate = Join-Path $Directory ("{0} ({1}).mp4" -f $BaseName,$n)
        $n++
    }

    return $candidate
}

function Get-LanguageName {
    param([string]$Code)

    if ([string]::IsNullOrWhiteSpace($Code)) {
        return 'Idioma no indicado'
    }

    switch -Regex ($Code.ToLower()) {

        '^(spa|es)$' { return 'Español' }
        '^(cat|ca)$' { return 'Catalán' }
        '^(eng|en)$' { return 'Inglés' }
        '^(fra|fre|fr)$' { return 'Francés' }
        '^(deu|ger|de)$' { return 'Alemán' }
        '^(ita|it)$' { return 'Italiano' }
        '^(por|pt)$' { return 'Portugués' }
        '^(jpn|ja)$' { return 'Japonés' }
        '^(kor|ko)$' { return 'Coreano' }
        '^(chi|zho|zh)$' { return 'Chino' }
        '^(rus|ru)$' { return 'Ruso' }
        '^(ara|ar)$' { return 'Árabe' }
        '^(nld|dut|nl)$' { return 'Neerlandés' }
        '^(pol|pl)$' { return 'Polaco' }
        '^(tur|tr)$' { return 'Turco' }
        '^(swe|sv)$' { return 'Sueco' }
        '^(dan|da)$' { return 'Danés' }
        '^(nor|no)$' { return 'Noruego' }
        '^(fin|fi)$' { return 'Finés' }
        '^(ces|cze|cs)$' { return 'Checo' }
        '^(hun|hu)$' { return 'Húngaro' }
        '^(ron|rum|ro)$' { return 'Rumano' }
        '^(ukr|uk)$' { return 'Ucraniano' }
        '^(heb|he)$' { return 'Hebreo' }
        '^(hin|hi)$' { return 'Hindi' }
        '^(tha|th)$' { return 'Tailandés' }
        '^(vie|vi)$' { return 'Vietnamita' }
        '^(ind|id)$' { return 'Indonesio' }
        '^(und)$' { return 'Idioma no indicado' }

        default { return $Code }
    }
}

function Get-ChannelText {
    param([int]$Channels)

    switch ($Channels) {
        1 { return '1.0 Mono' }
        2 { return '2.0 Stereo' }
        6 { return '5.1' }
        8 { return '7.1' }

        default {
            if ($Channels -gt 0) {
                return "$Channels canales"
            }

            return '? canales'
        }
    }
}

function Get-CodecText {
    param([string]$Codec)

    switch ($Codec.ToLower()) {
        'aac' { return 'AAC' }
        'ac3' { return 'AC-3' }
        'eac3' { return 'E-AC-3' }
        'dts' { return 'DTS' }
        'dca' { return 'DTS' }
        'truehd' { return 'TrueHD' }
        'flac' { return 'FLAC' }
        'opus' { return 'Opus' }
        'vorbis' { return 'Vorbis' }
        'mp3' { return 'MP3' }
        'pcm_s16le' { return 'PCM 16-bit' }
        'pcm_s24le' { return 'PCM 24-bit' }
        'pcm_s32le' { return 'PCM 32-bit' }

        default {
            if ([string]::IsNullOrWhiteSpace($Codec)) {
                return 'Desconocido'
            }

            return $Codec
        }
    }
}

function Get-SubtitleCodecName {
    param([string]$Codec)

    switch ($Codec.ToLower()) {
        'hdmv_pgs_subtitle' { return 'PGS' }
        'dvd_subtitle' { return 'DVD' }
        'dvb_subtitle' { return 'DVB' }
        'dvb_teletext' { return 'Teletexto' }
        'subrip' { return 'SRT' }
        'ass' { return 'ASS' }
        'ssa' { return 'SSA' }
        'webvtt' { return 'WebVTT' }
        'mov_text' { return 'mov_text' }

        default {
            return $Codec
        }
    }
}

# ============================================================
# FFPROBE
# ============================================================

function Invoke-FFprobeJson {
    param(
        [string]$File
    )

    $psi = New-Object System.Diagnostics.ProcessStartInfo

    $psi.FileName = $FFprobe
    $psi.Arguments =
        '-v error -show_streams -show_format -of json "' +
        $File.Replace('"','\"') +
        '"'

    $psi.UseShellExecute = $false
    $psi.CreateNoWindow = $true
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true

    $process = New-Object System.Diagnostics.Process
    $process.StartInfo = $psi

    [void]$process.Start()

    $stdout = $process.StandardOutput.ReadToEnd()
    $stderr = $process.StandardError.ReadToEnd()

    $process.WaitForExit()

    if ($process.ExitCode -ne 0) {
        throw "ffprobe no pudo analizar el archivo.`r`n`r`n$stderr"
    }

    if ([string]::IsNullOrWhiteSpace($stdout)) {
        throw 'ffprobe no devolvió información.'
    }

    return $stdout | ConvertFrom-Json
}

# ============================================================
# ANALIZAR AUDIO
# ============================================================

function Convert-AudioTrack {
    param(
        $Stream,
        [int]$Number
    )

    $languageCode = 'und'

    if ($null -ne $Stream.tags) {
        if ($null -ne $Stream.tags.language) {
            $languageCode = [string]$Stream.tags.language
        }
    }

    $languageName = Get-LanguageName $languageCode

    $title = 'Sin título'

    if ($null -ne $Stream.tags) {
        if ($null -ne $Stream.tags.title) {
            if (-not [string]::IsNullOrWhiteSpace([string]$Stream.tags.title)) {
                $title = [string]$Stream.tags.title
            }
        }
    }

    $codec = [string]$Stream.codec_name
    $codecDisplay = Get-CodecText $codec

    $channels = 0

    if ($null -ne $Stream.channels) {
        $channels = [int]$Stream.channels
    }

    $channelText = Get-ChannelText $channels

    $sampleRate = 0

    if ($null -ne $Stream.sample_rate) {
        [int]::TryParse(
            [string]$Stream.sample_rate,
            [ref]$sampleRate
        ) | Out-Null
    }

    $sampleRateText = '?'

    if ($sampleRate -gt 0) {
        $sampleRateText = '{0:N3}' -f ($sampleRate / 1000)
    }

    $bits = '?'

    if ($null -ne $Stream.bits_per_raw_sample) {
        if ([string]$Stream.bits_per_raw_sample -ne '0') {
            $bits = [string]$Stream.bits_per_raw_sample
        }
    }

    if ($bits -eq '?') {
        if ($null -ne $Stream.bits_per_sample) {
            if ([string]$Stream.bits_per_sample -ne '0') {
                $bits = [string]$Stream.bits_per_sample
            }
        }
    }

    # BITRATE: leer el valor REAL que ya esté almacenado/expuesto por el MKV/FFprobe.
    # No se calcula a partir de tamaño/duración.
    $bps = $null

    if ($null -ne $Stream.tags) {
        # Matroska puede guardar BPS con distintos sufijos de idioma, por ejemplo
        # BPS, BPS-eng, BPS-und, etc. Recorremos todas las propiedades para no
        # depender de un único nombre concreto.
        foreach ($tag in @($Stream.tags.PSObject.Properties)) {
            if ([string]$tag.Name -match '(?i)^BPS(?:-[^-]+)?$') {
                $candidate = [string]$tag.Value
                if (-not [string]::IsNullOrWhiteSpace($candidate)) {
                    [double]$candidateNumber = 0
                    if ([double]::TryParse(
                        $candidate,
                        [Globalization.NumberStyles]::Float,
                        [Globalization.CultureInfo]::InvariantCulture,
                        [ref]$candidateNumber
                    )) {
                        if ($candidateNumber -gt 0) {
                            $bps = $candidate
                            break
                        }
                    }
                }
            }
        }
    }

    # Segundo origen: bitrate que FFprobe expone directamente en el stream.
    if ([string]::IsNullOrWhiteSpace($bps)) {
        if ($null -ne $Stream.bit_rate) {
            $candidate = [string]$Stream.bit_rate
            [double]$candidateNumber = 0
            if ([double]::TryParse(
                $candidate,
                [Globalization.NumberStyles]::Float,
                [Globalization.CultureInfo]::InvariantCulture,
                [ref]$candidateNumber
            )) {
                if ($candidateNumber -gt 0) {
                    $bps = $candidate
                }
            }
        }
    }

    $bitrate = '?'

    if (-not [string]::IsNullOrWhiteSpace($bps)) {

        [double]$bpsNumber = 0

        if ([double]::TryParse(
            $bps,
            [Globalization.NumberStyles]::Float,
            [Globalization.CultureInfo]::InvariantCulture,
            [ref]$bpsNumber
        )) {

            if ($bpsNumber -gt 0) {
                $bitrate = [math]::Round($bpsNumber / 1000)
            }
        }
    }

    $duration = 0

    if ($null -ne $Stream.duration) {
        [double]::TryParse(
            [string]$Stream.duration,
            [Globalization.NumberStyles]::Float,
            [Globalization.CultureInfo]::InvariantCulture,
            [ref]$duration
        ) | Out-Null
    }

    if ($duration -le 0 -and $null -ne $Stream.tags) {

        foreach ($tag in @($Stream.tags.PSObject.Properties)) {
            if ([string]$tag.Name -match '(?i)^DURATION(?:-[^-]+)?$') {
                try {
                    $d = [string]$tag.Value
                    $parts = $d.Split(':')
                    if ($parts.Count -eq 3) {
                        $duration =
                            ([double]$parts[0] * 3600) +
                            ([double]$parts[1] * 60) +
                            ([double]$parts[2])
                    }
                }
                catch {}
                if ($duration -gt 0) { break }
            }
        }
    }

    $bytes = 0

    if ($null -ne $Stream.tags) {
        foreach ($tag in @($Stream.tags.PSObject.Properties)) {
            if ([string]$tag.Name -match '(?i)^NUMBER_OF_BYTES(?:-[^-]+)?$') {
                [Int64]$candidateBytes = 0
                if ([Int64]::TryParse([string]$tag.Value,[ref]$candidateBytes)) {
                    if ($candidateBytes -gt 0) {
                        $bytes = $candidateBytes
                        break
                    }
                }
            }
        }
    }

    $default = $false
    $original = $false

    if ($null -ne $Stream.disposition) {

        if ($null -ne $Stream.disposition.default) {
            $default = ([int]$Stream.disposition.default -eq 1)
        }

        if ($null -ne $Stream.disposition.original) {
            $original = ([int]$Stream.disposition.original -eq 1)
        }
    }

    [PSCustomObject]@{
        Number          = $Number
        Index           = [int]$Stream.index
        LanguageCode    = $languageCode
        Language        = $languageName
        Title           = $title
        Codec           = $codec
        CodecDisplay    = $codecDisplay
        Channels        = $channels
        ChannelText     = $channelText
        Bitrate         = $bitrate
        SampleRate      = $sampleRate
        SampleRateText  = $sampleRateText
        Bits            = $bits
        Duration        = $duration
        Bytes           = $bytes
        Default         = $default
        Original        = $original
        BpsRaw          = $bps
    }
}

# ============================================================
# ANALIZAR SUBTITULOS
# ============================================================

function Convert-SubtitleTrack {
    param(
        $Stream,
        [int]$Number
    )

    $codec = [string]$Stream.codec_name

    $compatible = $true

    switch ($codec.ToLower()) {

        'hdmv_pgs_subtitle' { $compatible = $false }
        'dvd_subtitle' { $compatible = $false }
        'dvb_subtitle' { $compatible = $false }
        'dvb_teletext' { $compatible = $false }
    }

    $languageCode = 'und'

    if ($null -ne $Stream.tags) {
        if ($null -ne $Stream.tags.language) {
            $languageCode = [string]$Stream.tags.language
        }
    }

    $title = ''

    if ($null -ne $Stream.tags) {
        if ($null -ne $Stream.tags.title) {
            $title = [string]$Stream.tags.title
        }
    }

    [PSCustomObject]@{
        Number       = $Number
        Index        = [int]$Stream.index
        LanguageCode = $languageCode
        Language     = Get-LanguageName $languageCode
        Title        = $title
        Codec        = $codec
        CodecDisplay = Get-SubtitleCodecName $codec
        Compatible   = $compatible
    }
}

# ============================================================
# INFORMACION DE VIDEO
# ============================================================

function Get-VideoCodecText {
    param([string]$Codec)

    switch ($Codec.ToLower()) {
        'h264' { return 'H.264' }
        'hevc' { return 'HEVC (H.265)' }
        'av1'  { return 'AV1' }
        'vp9'  { return 'VP9' }
        'vp8'  { return 'VP8' }

        default {
            if ([string]::IsNullOrWhiteSpace($Codec)) {
                return 'Desconocido'
            }

            return $Codec
        }
    }
}

function Get-VideoBitDepth {
    param($Stream)

    $bits = 0

    if ($null -ne $Stream.bits_per_raw_sample) {
        [int]::TryParse(
            [string]$Stream.bits_per_raw_sample,
            [ref]$bits
        ) | Out-Null
    }

    if ($bits -le 0 -and $null -ne $Stream.bits_per_sample) {
        [int]::TryParse(
            [string]$Stream.bits_per_sample,
            [ref]$bits
        ) | Out-Null
    }

    if ($bits -le 0 -and $null -ne $Stream.pix_fmt) {

        if ([string]$Stream.pix_fmt -match '(p010|yuv420p10|yuv422p10|yuv444p10|10le|10be)') {
            $bits = 10
        }
        elseif ([string]$Stream.pix_fmt -match '(p012|12le|12be|yuv420p12|yuv422p12|yuv444p12)') {
            $bits = 12
        }
    }

    if ($bits -gt 0) {
        return "$bits-bit"
    }

    return ''
}

function Get-VideoProfileText {
    param(
        [string]$Codec,
        [string]$Profile
    )

    if ([string]::IsNullOrWhiteSpace($Profile)) {
        return ''
    }

    switch ($Codec.ToLower()) {

        'hevc' {

            switch -Regex ($Profile) {

                '^Main 10$' { return 'Main 10' }
                '^Main 12$' { return 'Main 12' }
                '^Main$' { return 'Main' }

                default {
                    return $Profile
                }
            }
        }

        default {
            return $Profile
        }
    }
}

function Get-VideoLevelText {
    param([string]$Level)

    if (
        [string]::IsNullOrWhiteSpace($Level) -or
        $Level -eq '0'
    ) {
        return ''
    }

    if ($Level -match '^\d+$') {

        $n = [int]$Level

        if ($n -ge 10 -and $n -lt 100) {

            return (
                'Level {0}.{1}' -f
                [math]::Floor($n / 10),
                ($n % 10)
            )
        }
    }

    if ($Level -match '^\d+\.\d+$') {
        return "Level $Level"
    }

    return "Level $Level"
}

function Get-VideoFpsText {
    param($Stream)

    $fps = 0.0
    $raw = ''

    if ($null -ne $Stream.avg_frame_rate) {
        $raw = [string]$Stream.avg_frame_rate
    }

    if (
        [string]::IsNullOrWhiteSpace($raw) -or
        $raw -eq '0/0'
    ) {

        if ($null -ne $Stream.r_frame_rate) {
            $raw = [string]$Stream.r_frame_rate
        }
    }

    if ($raw -match '^(\d+(?:\.\d+)?)/(\d+(?:\.\d+)?)$') {

        $den = [double]$Matches[2]

        if ($den -ne 0) {
            $fps = [double]$Matches[1] / $den
        }

    }
    elseif ($raw -match '^\d+(?:\.\d+)?$') {

        $fps = [double]$raw
    }

    if ($fps -le 0) {
        return ''
    }

    return ('{0:0.###} fps' -f $fps)
}

function Get-VideoBitrateText {
    param(
        $Stream,
        $Format
    )

    $bps = 0.0

    if ($null -ne $Stream.bit_rate) {

        [double]::TryParse(
            [string]$Stream.bit_rate,
            [Globalization.NumberStyles]::Float,
            [Globalization.CultureInfo]::InvariantCulture,
            [ref]$bps
        ) | Out-Null
    }

    if ($bps -le 0 -and $null -ne $Stream.tags) {

        if ($null -ne $Stream.tags.'BPS-eng') {

            [double]::TryParse(
                [string]$Stream.tags.'BPS-eng',
                [Globalization.NumberStyles]::Float,
                [Globalization.CultureInfo]::InvariantCulture,
                [ref]$bps
            ) | Out-Null
        }
    }

    if ($bps -le 0 -and $null -ne $Format) {

        [double]$formatBps = 0

        if ($null -ne $Format.bit_rate) {

            [double]::TryParse(
                [string]$Format.bit_rate,
                [Globalization.NumberStyles]::Float,
                [Globalization.CultureInfo]::InvariantCulture,
                [ref]$formatBps
            ) | Out-Null
        }

        [double]$duration = 0

        if ($null -ne $Format.duration) {

            [double]::TryParse(
                [string]$Format.duration,
                [Globalization.NumberStyles]::Float,
                [Globalization.CultureInfo]::InvariantCulture,
                [ref]$duration
            ) | Out-Null
        }

        if (
            $formatBps -gt 0 -and
            $duration -gt 0
        ) {

            $audioBps = 0.0

            if ($null -ne $Format._audio_bps) {
                $audioBps = [double]$Format._audio_bps
            }

            $bps = $formatBps - $audioBps

            if ($bps -lt 0) {
                $bps = 0
            }
        }
    }

    if ($bps -le 0) {
        return ''
    }

    $kbps = [math]::Round($bps / 1000)

    if ($kbps -lt 1) {
        return ''
    }

    return ('{0:N0} Kbps' -f $kbps)
}

function Get-VideoVfrText {
    param($Stream)

    $avg = [string]$Stream.avg_frame_rate
    $raw = [string]$Stream.r_frame_rate

    if (
        [string]::IsNullOrWhiteSpace($avg) -or
        [string]::IsNullOrWhiteSpace($raw) -or
        $avg -eq '0/0' -or
        $raw -eq '0/0'
    ) {
        return ''
    }

    try {

        $a = 0.0
        $r = 0.0

        if (
            $avg -match '^(\d+)/(\d+)$' -and
            [double]$Matches[2] -ne 0
        ) {
            $a = [double]$Matches[1] / [double]$Matches[2]
        }

        if (
            $raw -match '^(\d+)/(\d+)$' -and
            [double]$Matches[2] -ne 0
        ) {
            $r = [double]$Matches[1] / [double]$Matches[2]
        }

        if (
            $a -gt 0 -and
            $r -gt 0 -and
            [math]::Abs($a - $r) -gt 0.01
        ) {
            return 'VFR'
        }
    }
    catch {}

    return ''
}

function Get-VideoHdrText {
    param($Stream)

    $sideData = @($Stream.side_data_list)

    foreach ($sd in $sideData) {

        $type = [string]$sd.side_data_type

        if ($type -match 'DOVI|Dolby Vision') {
            return 'Dolby Vision'
        }

        if (
            $type -match
            'Mastering display metadata|Content light level'
        ) {
            return 'HDR10'
        }
    }

    $transfer = [string]$Stream.color_transfer
    $primaries = [string]$Stream.color_primaries

    if ($transfer -eq 'arib-std-b67') {
        return 'HLG'
    }

    if (
        $transfer -eq 'smpte2084' -and
        $primaries -eq 'bt2020'
    ) {
        return 'HDR10'
    }

    if (
        $transfer -match
        'smpte2084|arib-std-b67'
    ) {
        return 'HDR'
    }

    return ''
}

# ============================================================
# ANALIZAR ARCHIVO
# ============================================================

function Analyze-File {
    param(
        [string]$File
    )

    if (-not (Test-Path -LiteralPath $File)) {
        throw "No existe el archivo:`r`n$File"
    }

    $info = Get-Item -LiteralPath $File
    $probe = Invoke-FFprobeJson $File

    $audioTracks = @()
    $subtitleTracks = @()

    $audioNumber = 0
    $subtitleNumber = 0

    foreach ($stream in @($probe.streams)) {

        if ([string]$stream.codec_type -eq 'audio') {

            $audioNumber++

            $audioTracks +=
                Convert-AudioTrack `
                    -Stream $stream `
                    -Number $audioNumber
        }

        if ([string]$stream.codec_type -eq 'subtitle') {

            $subtitleNumber++

            $subtitleTracks +=
                Convert-SubtitleTrack `
                    -Stream $stream `
                    -Number $subtitleNumber
        }
    }

    $duration = 0

    if ($null -ne $probe.format) {

        if ($null -ne $probe.format.duration) {

            [double]::TryParse(
                [string]$probe.format.duration,
                [Globalization.NumberStyles]::Float,
                [Globalization.CultureInfo]::InvariantCulture,
                [ref]$duration
            ) | Out-Null
        }
    }

    $videoStreams = @(
        @($probe.streams) |
        Where-Object {
            $_.codec_type -eq 'video'
        }
    )

    $width = 0
    $height = 0
    $videoCodec = ''
    $videoCodecText = ''
    $videoBitDepth = ''
    $videoProfile = ''
    $videoLevel = ''
    $videoHdr = ''
    $videoFps = ''
    $videoBitrate = ''
    $videoVfr = ''
    $vs = $null

    if ($videoStreams.Count -gt 0) {

        $vs = $videoStreams[0]

        $width = [int]$vs.width
        $height = [int]$vs.height

        $videoCodec = [string]$vs.codec_name

        $videoCodecText =
            Get-VideoCodecText $videoCodec

        $videoBitDepth =
            Get-VideoBitDepth $vs

        $videoProfile =
            Get-VideoProfileText `
                $videoCodec `
                ([string]$vs.profile)

        $videoLevel =
            Get-VideoLevelText `
                ([string]$vs.level)

        $videoHdr =
            Get-VideoHdrText $vs

        $videoFps =
            Get-VideoFpsText $vs

        $audioBpsForVideoFallback = 0.0

        foreach ($a in @($audioTracks)) {

            if (
                -not [string]::IsNullOrWhiteSpace(
                    [string]$a.BpsRaw
                )
            ) {

                [double]$ab = 0

                [double]::TryParse(
                    [string]$a.BpsRaw,
                    [Globalization.NumberStyles]::Float,
                    [Globalization.CultureInfo]::InvariantCulture,
                    [ref]$ab
                ) | Out-Null

                if ($ab -gt 0) {
                    $audioBpsForVideoFallback += $ab
                }
            }
        }

        if ($null -ne $probe.format) {

            $probe.format |
                Add-Member `
                    -NotePropertyName _audio_bps `
                    -NotePropertyValue $audioBpsForVideoFallback `
                    -Force
        }

        $videoBitrate =
            Get-VideoBitrateText `
                $vs `
                $probe.format

        $videoVfr =
            Get-VideoVfrText $vs
    }

    [PSCustomObject]@{
        Path           = $File
        Name           = $info.Name
        BaseName       = $info.BaseName
        SizeBytes      = [Int64]$info.Length
        SizeText       = Format-Bytes $info.Length
        Duration       = $duration
        DurationText   = Format-Duration $duration
        Width          = $width
        Height         = $height
        VideoCodec     = $videoCodec
        VideoCodecText = $videoCodecText
        VideoBitDepth  = $videoBitDepth
        VideoProfile   = $videoProfile
        VideoLevel     = $videoLevel
        VideoHdr       = $videoHdr
        VideoFps       = $videoFps
        VideoBitrate   = $videoBitrate
        VideoVfr       = $videoVfr
        AudioTracks    = $audioTracks
        SubtitleTracks = $subtitleTracks
        Probe          = $probe
    }
}

# ============================================================
# CALCULAR BITRATE AAC
# ============================================================

function Get-SelectedAudioBitrate {
    param($Track)

    $value = 0

    if (
        -not [string]::IsNullOrWhiteSpace(
            [string]$Track.BpsRaw
        )
    ) {

        [double]::TryParse(
            [string]$Track.BpsRaw,
            [Globalization.NumberStyles]::Float,
            [Globalization.CultureInfo]::InvariantCulture,
            [ref]$value
        ) | Out-Null
    }

    if ($value -le 0) {

        if (
            $Track.Bitrate -ne '?' -and
            $null -ne $Track.Bitrate
        ) {
            $value = [double]$Track.Bitrate * 1000
        }
    }

    if ($value -le 0) {
        return 320
    }

    $kbps = [math]::Round($value / 1000)

    if ($kbps -lt 1) {
        $kbps = 320
    }

    return [int]$kbps
}

# ============================================================
# CALCULAR BITRATE VIDEO
# ============================================================

function Get-VideoBitrate {
    param(
        [double]$Duration,
        [int]$AudioKbps
    )

    if ($Duration -le 0) {
        return 5000
    }

    $audioMB =
        ($AudioKbps * $Duration) / 8192

    $targetVideoMB =
        ($script:LimitBytes / 1000000) - $audioMB

    $videoKbps =
        [math]::Round(
            ($targetVideoMB * 8192) / $Duration
        )

    return [int]$videoKbps
}

# ============================================================
# INTERFAZ
# ============================================================

$form = New-Object System.Windows.Forms.Form
$form.Text = 'MKV Converter'
$form.StartPosition = 'CenterScreen'
$form.Size = New-Object System.Drawing.Size(1536,1024)
$form.MinimumSize = New-Object System.Drawing.Size(800,600)
$form.BackColor = [System.Drawing.Color]::FromArgb(12,15,20)
$form.ForeColor = [System.Drawing.Color]::FromArgb(228,232,238)
$form.Font = New-Object System.Drawing.Font('Segoe UI',10)

$nightBg = [System.Drawing.Color]::FromArgb(12,15,20)
$panelBg = [System.Drawing.Color]::FromArgb(16,21,29)
$panelBg2 = [System.Drawing.Color]::FromArgb(20,26,35)
$controlBg = [System.Drawing.Color]::FromArgb(23,29,39)
$border = [System.Drawing.Color]::FromArgb(39,49,63)
$text = [System.Drawing.Color]::FromArgb(226,231,239)
$muted = [System.Drawing.Color]::FromArgb(166,177,194)
$soft = [System.Drawing.Color]::FromArgb(126,140,160)
$accent = [System.Drawing.Color]::FromArgb(205,212,222)

function Set-NightButton([System.Windows.Forms.Button]$b) {
    $b.FlatStyle = 'Flat'
    $b.FlatAppearance.BorderSize = 1
    $b.FlatAppearance.BorderColor = $border
    $b.BackColor = $controlBg
    $b.ForeColor = $text
    $b.Font = New-Object System.Drawing.Font('Segoe UI Semibold',10)
}
function Set-NightGroup([System.Windows.Forms.GroupBox]$g) {
    $g.ForeColor = $text
    $g.BackColor = $panelBg
    $g.Font = New-Object System.Drawing.Font('Segoe UI Semibold',10)
}

# CABECERA
$title = New-Object System.Windows.Forms.Label
$title.Text = 'MKV Converter'
$title.Font = New-Object System.Drawing.Font('Segoe UI Semibold',21)
$title.ForeColor = $text
$title.Location = New-Object System.Drawing.Point(98,17)
$title.Size = New-Object System.Drawing.Size(500,42)
$form.Controls.Add($title)
$iconPanel = New-Object System.Windows.Forms.Panel
$iconPanel.Location = New-Object System.Drawing.Point(27,16)
$iconPanel.Size = New-Object System.Drawing.Size(44,44)
$iconPanel.BackColor = $controlBg
$iconPanel.BorderStyle = 'FixedSingle'
$form.Controls.Add($iconPanel)
$iconLabel = New-Object System.Windows.Forms.Label
$iconLabel.Text = '▣'
$iconLabel.Font = New-Object System.Drawing.Font('Segoe UI Symbol',23)
$iconLabel.ForeColor = $accent
$iconLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$iconLabel.Dock = 'Fill'
$iconPanel.Controls.Add($iconLabel)
$subtitle = New-Object System.Windows.Forms.Label
$subtitle.Text = 'Conversión inteligente · NVENC · Selector de audio'
$subtitle.Font = New-Object System.Drawing.Font('Segoe UI',9)
$subtitle.ForeColor = $soft
$subtitle.Location = New-Object System.Drawing.Point(100,55)
$subtitle.Size = New-Object System.Drawing.Size(700,22)
$form.Controls.Add($subtitle)

# FILA SUPERIOR
$groupInfo = New-Object System.Windows.Forms.GroupBox
$groupInfo.Text = '  Información del archivo  '
$groupInfo.Location = New-Object System.Drawing.Point(27,88)
$groupInfo.Size = New-Object System.Drawing.Size(504,292)
Set-NightGroup $groupInfo
$form.Controls.Add($groupInfo)
$lblFileInfo = New-Object System.Windows.Forms.Label
$lblFileInfo.Text = 'Añade un archivo MKV para comenzar.'
$lblFileInfo.ForeColor = $text
$lblFileInfo.Font = New-Object System.Drawing.Font('Segoe UI',11)
$lblFileInfo.Location = New-Object System.Drawing.Point(28,52)
$lblFileInfo.Size = New-Object System.Drawing.Size(445,205)
$groupInfo.Controls.Add($lblFileInfo)

$groupTarget = New-Object System.Windows.Forms.GroupBox
$groupTarget.Text = '  Tamaño objetivo  '
$groupTarget.Location = New-Object System.Drawing.Point(531,88)
$groupTarget.Size = New-Object System.Drawing.Size(636,292)
Set-NightGroup $groupTarget
$form.Controls.Add($groupTarget)
$lblTargetSize = New-Object System.Windows.Forms.Label
$lblTargetSize.Text = 'Tamaño objetivo:'
$lblTargetSize.ForeColor = $text
$lblTargetSize.Font = New-Object System.Drawing.Font('Segoe UI Semibold',12)
$lblTargetSize.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$lblTargetSize.Location = New-Object System.Drawing.Point(240,51)
$lblTargetSize.Size = New-Object System.Drawing.Size(180,24)
$groupTarget.Controls.Add($lblTargetSize)

$pnlTargetSizeEditBorder = New-Object System.Windows.Forms.Panel
$pnlTargetSizeEditBorder.Location = New-Object System.Drawing.Point(240,80)
$pnlTargetSizeEditBorder.Size = New-Object System.Drawing.Size(180,56)
$pnlTargetSizeEditBorder.BackColor = $controlBg
$pnlTargetSizeEditBorder.Padding = New-Object System.Windows.Forms.Padding(0)
$pnlTargetSizeEditBorder.Add_Paint({ param($sender,$e); $pen=New-Object System.Drawing.Pen($border,1); try{$e.Graphics.DrawRectangle($pen,0,0,$sender.ClientSize.Width-1,$sender.ClientSize.Height-1)}finally{$pen.Dispose()} })
$groupTarget.Controls.Add($pnlTargetSizeEditBorder)
$txtTargetSizeEdit = New-Object System.Windows.Forms.TextBox
$txtTargetSizeEdit.Text = Get-TargetSizeText
$txtTargetSizeEdit.ForeColor = $text
$txtTargetSizeEdit.BackColor = $controlBg
$txtTargetSizeEdit.Font = New-Object System.Drawing.Font('Segoe UI Semibold',18)
$txtTargetSizeEdit.Location = New-Object System.Drawing.Point(5,6)
$txtTargetSizeEdit.Size = New-Object System.Drawing.Size(168,42)
$txtTargetSizeEdit.AutoSize = $false
$txtTargetSizeEdit.BorderStyle = 'None'
$txtTargetSizeEdit.TextAlign = [System.Windows.Forms.HorizontalAlignment]::Center
$txtTargetSizeEdit.Margin = New-Object System.Windows.Forms.Padding(0)
$txtTargetSizeEdit.Multiline = $false
$txtTargetSizeEdit.TabStop = $false
$txtTargetSizeEdit.Cursor = [System.Windows.Forms.Cursors]::IBeam
$txtTargetSizeEdit.ShortcutsEnabled = $true
$txtTargetSizeEdit.HideSelection = $false
$pnlTargetSizeEditBorder.Controls.Add($txtTargetSizeEdit)
$lblTargetSizeValue = $txtTargetSizeEdit

$trkTargetSize = New-Object System.Windows.Forms.TrackBar
$trkTargetSize.Minimum = 0
$trkTargetSize.Maximum = 50000
$trkTargetSize.Value = Get-SliderValueFromTargetMB $script:TargetSizeMB
$trkTargetSize.SmallChange = 1
$trkTargetSize.LargeChange = 5
$trkTargetSize.TickFrequency = 25
$trkTargetSize.AutoSize = $false
$trkTargetSize.Location = New-Object System.Drawing.Point(26,152)
$trkTargetSize.Size = New-Object System.Drawing.Size(580,38)
$trkTargetSize.BackColor = $panelBg
$groupTarget.Controls.Add($trkTargetSize)
foreach($spec in @(@('200 MB',20),@('1 GB',136),@('2 GB',286),@('5 GB',426),@('10 GB',548))) { $l=New-Object System.Windows.Forms.Label; $l.Text=$spec[0]; $l.Location=New-Object System.Drawing.Point([int]$spec[1],199); $l.Size=New-Object System.Drawing.Size(70,24); $l.ForeColor=$muted; $groupTarget.Controls.Add($l) }
$lblTargetSizeHint = New-Object System.Windows.Forms.Label
$lblTargetSizeHint.Text = '− 50 MB de margen de seguridad en NVENC'
$lblTargetSizeHint.ForeColor = $soft
$lblTargetSizeHint.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
$lblTargetSizeHint.Location = New-Object System.Drawing.Point(160,232)
$lblTargetSizeHint.Size = New-Object System.Drawing.Size(320,22)
$lblTargetSizeHint.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
$groupTarget.Controls.Add($lblTargetSizeHint)

$groupActions = New-Object System.Windows.Forms.Panel
$groupActions.Location = New-Object System.Drawing.Point(1187,88)
$groupActions.Size = New-Object System.Drawing.Size(322,292)
$groupActions.BackColor = $panelBg
$groupActions.BorderStyle = 'FixedSingle'
$form.Controls.Add($groupActions)
$btnConvert = New-Object System.Windows.Forms.Button
$btnConvert.Text = '▶  Iniciar conversión'
$btnConvert.Location = New-Object System.Drawing.Point(10,10)
$btnConvert.Size = New-Object System.Drawing.Size(302,70)
Set-NightButton $btnConvert
$btnConvert.Font = New-Object System.Drawing.Font('Segoe UI Semibold',13)
$groupActions.Controls.Add($btnConvert)
$btnBrowse = New-Object System.Windows.Forms.Button
$btnBrowse.Text = '▣  Examinar carpeta de salida'
$btnBrowse.Location = New-Object System.Drawing.Point(10,90)
$btnBrowse.Size = New-Object System.Drawing.Size(302,70)
Set-NightButton $btnBrowse
$groupActions.Controls.Add($btnBrowse)
$btnInfo = New-Object System.Windows.Forms.Button
$btnInfo.Text = '⚙  Opciones / Cómo funciona'
$btnInfo.Location = New-Object System.Drawing.Point(10,170)
$btnInfo.Size = New-Object System.Drawing.Size(302,70)
Set-NightButton $btnInfo
$groupActions.Controls.Add($btnInfo)

# SEGUNDA FILA
$groupAudio = New-Object System.Windows.Forms.GroupBox
$groupAudio.Text = '  Pistas de audio  '
$groupAudio.Location = New-Object System.Drawing.Point(27,399)
$groupAudio.Size = New-Object System.Drawing.Size(362,173)
Set-NightGroup $groupAudio
$form.Controls.Add($groupAudio)
$comboAudio = New-Object System.Windows.Forms.CheckedListBox
$comboAudio.BackColor=$controlBg; $comboAudio.ForeColor=$text; $comboAudio.Location=New-Object System.Drawing.Point(18,38); $comboAudio.Size=New-Object System.Drawing.Size(326,76); $comboAudio.BorderStyle='FixedSingle'; $comboAudio.CheckOnClick=$true; $comboAudio.HorizontalScrollbar=$true; $comboAudio.IntegralHeight=$false
$groupAudio.Controls.Add($comboAudio)
$lblAudioDetails=New-Object System.Windows.Forms.Label; $lblAudioDetails.Text='No hay audio seleccionado.'; $lblAudioDetails.ForeColor=$muted; $lblAudioDetails.Location=New-Object System.Drawing.Point(18,122); $lblAudioDetails.Size=New-Object System.Drawing.Size(326,32); $groupAudio.Controls.Add($lblAudioDetails)

$groupSubtitles=New-Object System.Windows.Forms.GroupBox; $groupSubtitles.Text='  Subtítulos  '; $groupSubtitles.Location=New-Object System.Drawing.Point(389,399); $groupSubtitles.Size=New-Object System.Drawing.Size(371,173); Set-NightGroup $groupSubtitles; $form.Controls.Add($groupSubtitles)
$lblSubtitlePanel=New-Object System.Windows.Forms.Label; $lblSubtitlePanel.Text="Subtítulos detectados`r`n`r`nLa selección se gestiona automáticamente según las pistas compatibles."; $lblSubtitlePanel.ForeColor=$muted; $lblSubtitlePanel.Location=New-Object System.Drawing.Point(18,38); $lblSubtitlePanel.Size=New-Object System.Drawing.Size(335,112); $lblSubtitlePanel.Font=New-Object System.Drawing.Font('Segoe UI',10); $groupSubtitles.Controls.Add($lblSubtitlePanel)

$groupMode=New-Object System.Windows.Forms.GroupBox; $groupMode.Text='  Modo de copia  '; $groupMode.Location=New-Object System.Drawing.Point(760,399); $groupMode.Size=New-Object System.Drawing.Size(356,173); Set-NightGroup $groupMode; $form.Controls.Add($groupMode)
$rbNormal=New-Object System.Windows.Forms.RadioButton; $rbNormal.Text='Normal (re-encode)'; $rbNormal.Location=New-Object System.Drawing.Point(25,51); $rbNormal.Size=New-Object System.Drawing.Size(300,32); $rbNormal.ForeColor=$text; $rbNormal.BackColor=$panelBg; $rbNormal.Checked=$true; $rbNormal.Enabled=$false; $groupMode.Controls.Add($rbNormal)
$rbDirect=New-Object System.Windows.Forms.RadioButton; $rbDirect.Text='Copiar directo (sin re-encode)'; $rbDirect.Location=New-Object System.Drawing.Point(25,91); $rbDirect.Size=New-Object System.Drawing.Size(300,32); $rbDirect.ForeColor=$text; $rbDirect.BackColor=$panelBg; $rbDirect.Enabled=$false; $groupMode.Controls.Add($rbDirect)
$lblMode=New-Object System.Windows.Forms.Label; $lblMode.Text='Modo: esperando archivo'; $lblMode.ForeColor=$muted; $lblMode.Font=New-Object System.Drawing.Font('Segoe UI Semibold',9.5); $lblMode.Location=New-Object System.Drawing.Point(25,132); $lblMode.Size=New-Object System.Drawing.Size(315,22); $groupMode.Controls.Add($lblMode)

$groupAdvanced=New-Object System.Windows.Forms.GroupBox; $groupAdvanced.Text='  Opciones avanzadas  '; $groupAdvanced.Location=New-Object System.Drawing.Point(1116,399); $groupAdvanced.Size=New-Object System.Drawing.Size(393,173); Set-NightGroup $groupAdvanced; $form.Controls.Add($groupAdvanced)
foreach($spec in @(@('Mantener estructura de carpetas',20,40),@('Forzar salida a MKV',20,76),@('Eliminar pistas no seleccionadas',20,112))) { $c=New-Object System.Windows.Forms.CheckBox; $c.Text=$spec[0]; $c.Location=New-Object System.Drawing.Point([int]$spec[1],[int]$spec[2]); $c.Size=New-Object System.Drawing.Size(340,28); $c.ForeColor=$text; $c.BackColor=$panelBg; $c.Enabled=$false; $groupAdvanced.Controls.Add($c) }

# COLA DE ARCHIVOS
$groupFiles=New-Object System.Windows.Forms.GroupBox; $groupFiles.Text='  Cola de archivos  '; $groupFiles.Location=New-Object System.Drawing.Point(27,590); $groupFiles.Size=New-Object System.Drawing.Size(1208,328); Set-NightGroup $groupFiles; $form.Controls.Add($groupFiles)
$listFiles=New-Object System.Windows.Forms.ListBox; $listFiles.BackColor=$panelBg2; $listFiles.ForeColor=$text; $listFiles.BorderStyle='FixedSingle'; $listFiles.SelectionMode='MultiExtended'; $listFiles.Location=New-Object System.Drawing.Point(18,38); $listFiles.Size=New-Object System.Drawing.Size(1170,265); $listFiles.AllowDrop=$true; $groupFiles.Controls.Add($listFiles)
$queuePanel=New-Object System.Windows.Forms.Panel; $queuePanel.Location=New-Object System.Drawing.Point(1260,590); $queuePanel.Size=New-Object System.Drawing.Size(249,328); $queuePanel.BackColor=$panelBg; $queuePanel.BorderStyle='FixedSingle'; $form.Controls.Add($queuePanel)
$btnAdd=New-Object System.Windows.Forms.Button; $btnAdd.Text='⊕  Agregar archivos'; $btnAdd.Location=New-Object System.Drawing.Point(10,10); $btnAdd.Size=New-Object System.Drawing.Size(229,58); Set-NightButton $btnAdd; $queuePanel.Controls.Add($btnAdd)
$btnRemove=New-Object System.Windows.Forms.Button; $btnRemove.Text='▱  Quitar seleccionados'; $btnRemove.Location=New-Object System.Drawing.Point(10,76); $btnRemove.Size=New-Object System.Drawing.Size(229,58); Set-NightButton $btnRemove; $queuePanel.Controls.Add($btnRemove)
$btnClear=New-Object System.Windows.Forms.Button; $btnClear.Text='⌧  Limpiar lista'; $btnClear.Location=New-Object System.Drawing.Point(10,142); $btnClear.Size=New-Object System.Drawing.Size(229,58); Set-NightButton $btnClear; $queuePanel.Controls.Add($btnClear)
$btnMoveUp=New-Object System.Windows.Forms.Button; $btnMoveUp.Text='↑  Mover arriba'; $btnMoveUp.Location=New-Object System.Drawing.Point(10,210); $btnMoveUp.Size=New-Object System.Drawing.Size(229,48); Set-NightButton $btnMoveUp; $btnMoveUp.Enabled=$false; $queuePanel.Controls.Add($btnMoveUp)
$btnMoveDown=New-Object System.Windows.Forms.Button; $btnMoveDown.Text='↓  Mover abajo'; $btnMoveDown.Location=New-Object System.Drawing.Point(10,264); $btnMoveDown.Size=New-Object System.Drawing.Size(229,48); Set-NightButton $btnMoveDown; $btnMoveDown.Enabled=$false; $queuePanel.Controls.Add($btnMoveDown)

# RUTA DE SALIDA: control funcional oculto, porque la referencia no muestra la ruta.
$groupOutput=New-Object System.Windows.Forms.GroupBox; $groupOutput.Visible=$false; $txtOutput=New-Object System.Windows.Forms.TextBox; $txtOutput.Text=$AppDir; $groupOutput.Controls.Add($txtOutput); $form.Controls.Add($groupOutput)

# BARRA INFERIOR
$statusPanel=New-Object System.Windows.Forms.Panel; $statusPanel.Location=New-Object System.Drawing.Point(0,943); $statusPanel.Size=New-Object System.Drawing.Size(1536,81); $statusPanel.BackColor=[System.Drawing.Color]::FromArgb(14,18,25); $statusPanel.BorderStyle='FixedSingle'; $form.Controls.Add($statusPanel)
$statusDot=New-Object System.Windows.Forms.Label; $statusDot.Text='◉'; $statusDot.Font=New-Object System.Drawing.Font('Segoe UI Symbol',13); $statusDot.ForeColor=$muted; $statusDot.Location=New-Object System.Drawing.Point(31,24); $statusDot.Size=New-Object System.Drawing.Size(30,28); $statusPanel.Controls.Add($statusDot)
$lblStatus=New-Object System.Windows.Forms.Label; $lblStatus.Text='Listo para convertir'; $lblStatus.ForeColor=$muted; $lblStatus.Location=New-Object System.Drawing.Point(66,24); $lblStatus.Size=New-Object System.Drawing.Size(850,28); $lblStatus.Font=New-Object System.Drawing.Font('Segoe UI',10); $statusPanel.Controls.Add($lblStatus)
$lblQueueCount=New-Object System.Windows.Forms.Label; $lblQueueCount.Text='0 / 0'; $lblQueueCount.ForeColor=$muted; $lblQueueCount.TextAlign=[System.Drawing.ContentAlignment]::MiddleRight; $lblQueueCount.Location=New-Object System.Drawing.Point(1030,23); $lblQueueCount.Size=New-Object System.Drawing.Size(65,30); $statusPanel.Controls.Add($lblQueueCount)
$progress=New-Object System.Windows.Forms.ProgressBar; $progress.Minimum=0; $progress.Maximum=100; $progress.Value=0; $progress.Style='Continuous'; $progress.Location=New-Object System.Drawing.Point(1093,27); $progress.Size=New-Object System.Drawing.Size(354,20); $statusPanel.Controls.Add($progress)
$lblProgress=New-Object System.Windows.Forms.Label; $lblProgress.Text='0%'; $lblProgress.ForeColor=$muted; $lblProgress.TextAlign=[System.Drawing.ContentAlignment]::MiddleRight; $lblProgress.Location=New-Object System.Drawing.Point(1452,22); $lblProgress.Size=New-Object System.Drawing.Size(55,30); $statusPanel.Controls.Add($lblProgress)
$lblLimit=New-Object System.Windows.Forms.Label; $lblLimit.Text=('Tamaño límite de cálculo: {0}' -f (Get-EffectiveLimitText)); $lblLimit.Visible=$false; $form.Controls.Add($lblLimit)

# Ventana informativa no modal: puede permanecer abierta mientras se usa
# la ventana principal. El RichTextBox permite seleccionar y copiar texto
# y dispone de barra de desplazamiento vertical.
$infoForm = $null

function Show-ProgramInfo {
    if ($null -ne $script:InfoForm -and -not $script:InfoForm.IsDisposed) {
        $script:InfoForm.WindowState = [System.Windows.Forms.FormWindowState]::Normal
        $script:InfoForm.BringToFront()
        $script:InfoForm.Activate()
        return
    }

    $script:InfoForm = New-Object System.Windows.Forms.Form
    $script:InfoForm.Text = 'MK5 Converter — Cómo funciona'
    $script:InfoForm.StartPosition = 'CenterScreen'
    $script:InfoForm.Size = New-Object System.Drawing.Size(820,680)
    $script:InfoForm.MinimumSize = New-Object System.Drawing.Size(620,480)
    $script:InfoForm.BackColor = [System.Drawing.Color]::FromArgb(25,25,28)
    $script:InfoForm.ForeColor = [System.Drawing.Color]::White
    $script:InfoForm.Font = New-Object System.Drawing.Font('Segoe UI',9.5)
    $script:InfoForm.ShowInTaskbar = $true

    # Barra superior de la ventana informativa, con el botón para guardar
    # una copia completa del BAT actual en formato TXT.
    $infoTopBar = New-Object System.Windows.Forms.Panel
    $infoTopBar.Dock = 'Top'
    $infoTopBar.Height = 52
    $infoTopBar.BackColor = [System.Drawing.Color]::FromArgb(25,25,28)
    $infoTopBar.Padding = New-Object System.Windows.Forms.Padding(0,8,10,8)

    $btnDownloadCode = New-Object System.Windows.Forms.Button
    $btnDownloadCode.Text = 'Descargar código'
    $btnDownloadCode.Font = New-Object System.Drawing.Font('Segoe UI Semibold',9)
    $btnDownloadCode.Size = New-Object System.Drawing.Size(135,34)
    $btnDownloadCode.Dock = 'Right'
    $btnDownloadCode.FlatStyle = 'Flat'
    $btnDownloadCode.ForeColor = [System.Drawing.Color]::White
    $btnDownloadCode.BackColor = [System.Drawing.Color]::FromArgb(55,75,95)
    $btnDownloadCode.FlatAppearance.BorderSize = 1
    $btnDownloadCode.FlatAppearance.BorderColor = [System.Drawing.Color]::FromArgb(75,100,125)
    $btnDownloadCode.FlatAppearance.MouseOverBackColor = [System.Drawing.Color]::FromArgb(65,90,115)
    $btnDownloadCode.FlatAppearance.MouseDownBackColor = [System.Drawing.Color]::FromArgb(45,65,85)
    $infoTopBar.Controls.Add($btnDownloadCode)

    $infoText = New-Object System.Windows.Forms.RichTextBox
    $infoText.Dock = 'Fill'
    $infoText.ReadOnly = $true
    $infoText.BackColor = [System.Drawing.Color]::FromArgb(32,32,36)
    $infoText.ForeColor = [System.Drawing.Color]::Gainsboro
    $infoText.BorderStyle = 'None'
    $infoText.Font = New-Object System.Drawing.Font('Segoe UI',10)
    $infoText.DetectUrls = $false
    $infoText.ScrollBars = [System.Windows.Forms.RichTextBoxScrollBars]::Vertical
    $infoText.WordWrap = $true
    $infoText.Margin = New-Object System.Windows.Forms.Padding(12)

    $infoContent = @'
MK5 CONVERTER — CÓMO FUNCIONA

¿QUÉ HACE EL PROGRAMA?
MK5 Converter convierte archivos MKV a MP4 usando un tamaño objetivo configurable. El programa analiza primero cada vídeo para conocer sus características y, según el caso, decide si puede copiarlo directamente o si necesita recodificarlo. La lista de archivos conserva el orden que aparece en pantalla y ese mismo orden es el que se utiliza al convertir.

1. AÑADIR ARCHIVOS
Puedes pulsar «Añadir» para seleccionar uno o varios MKV, o arrastrarlos directamente a la lista. Los archivos aparecen en el orden en el que se incorporan. Los duplicados no se añaden.

Puedes seleccionar varios archivos con Ctrl + clic y seleccionar un bloque con Shift + clic. Con las flechas ▲ y ▼ puedes mover uno o varios archivos dentro de la lista. Los archivos seleccionados mantienen su orden entre ellos.

2. SELECCIONAR UN ARCHIVO
Al seleccionar un MKV, el programa lo analiza y muestra su información. También carga las pistas de audio disponibles para que puedas elegir cuáles conservar. Reordenar archivos no obliga a volver a analizar el vídeo, por lo que las flechas son rápidas y fluidas.

3. AUDIO
El programa permite trabajar con las pistas de audio detectadas y seleccionar las que deben incluirse en el MP4. La selección de audio se mantiene separada del procesamiento de vídeo.

4. SUBTÍTULOS
Las pistas de subtítulos se gestionan durante la conversión según la información disponible en el MKV y la lógica de selección del programa.

5. COPIA DIRECTA
Cuando el vídeo puede mantenerse sin recodificación, el programa utiliza copia directa: -c:v copy y -c:a copy. En ese caso no se utiliza NVENC, no se cambia la profundidad de bits ni se recodifica el vídeo. Es la opción que conserva el vídeo original sin volver a comprimirlo.

6. RECODIFICACIÓN DE VÍDEO
Cuando es necesario recodificar, MK5 utiliza el codificador HEVC de NVIDIA mediante NVENC (hevc_nvenc). El preset es siempre P7, orientado a la máxima calidad de NVENC. Además utiliza VBR y multipass a resolución completa (fullres), de modo que la segunda pasada interna ayuda a repartir mejor los bits en las zonas que más lo necesitan sin cambiar el bitrate objetivo.

También se utiliza cuantización adaptativa espacial (Spatial AQ) con intensidad 10 para mejorar la distribución perceptual de los bits. Estas opciones priorizan la calidad y no están pensadas para obtener la máxima velocidad posible.

7. OBJETIVO DEL CONVERSOR: TAMAÑO CONFIGURABLE
El tamaño objetivo se puede elegir con el slider desde 0 MB hasta 1 TB. Si el vídeo pesa igual o menos que el tamaño mostrado, se utiliza COPIA DIRECTA. Si pesa más, se utiliza NVENC. Para NVENC se reservan siempre 50 MB de margen de seguridad en el cálculo efectivo.

• Si el archivo pesa igual o menos que el tamaño elegido, se conserva mediante copia directa cuando la ruta de salida es compatible: no se vuelve a comprimir el vídeo y se mantiene en modo lossless respecto al vídeo original.
• Si el archivo supera el tamaño elegido, se recodifica con NVENC buscando la máxima calidad perceptual posible dentro del tamaño efectivo disponible. El cálculo de NVENC reserva 50 MB respecto al tamaño seleccionado.

Para la recodificación, el bitrate de vídeo se calcula a partir del límite real en bytes, la duración y el bitrate de audio exacto que se va a utilizar en la salida. Además se estima de forma dinámica el espacio necesario para los átomos, índices, metadatos y pistas del contenedor MP4, en lugar de descontar siempre una reserva fija de 2 MB. Así se aprovecha mejor el espacio disponible sin cambiar el límite máximo.

El cálculo de bitrate conserva los límites de seguridad establecidos por el programa (200–20.000 Kbps). P7 y multipass se encargan de aprovechar lo mejor posible los bits disponibles, pero el objetivo de tamaño sigue siendo el mismo.

8. PROFUNDIDAD DE BITS
El programa intenta mantener la profundidad de bits de origen de forma apropiada para HEVC NVENC:
• Origen 8-bit → salida 8-bit (yuv420p).
• Origen 10-bit → salida 10-bit (p010le).
• Origen 12-bit → salida 10-bit (p010le), porque NVENC HEVC no genera una salida HEVC NVENC real de 12 bits en este flujo.
No se convierte innecesariamente un vídeo 8-bit a 10-bit.

9. COLOR Y HDR
Durante la recodificación se conservan explícitamente las características de color declaradas por el vídeo de origen cuando están disponibles: primarias, transferencia, matriz/colorspace y rango.

Si el origen es HDR, el programa mantiene el flujo HDR en lugar de convertirlo a SDR. Esto incluye los casos HDR10 y otras señales HDR compatibles con la información disponible en el archivo. Cuando existen metadatos HDR estáticos disponibles, como mastering display y MaxCLL/MaxFALL, FFmpeg puede transportarlos al proceso de salida. Un origen HDR de 12 bits pasa a 10 bits, pero sigue siendo HDR.

10. PROGRESO
Durante la conversión, la interfaz muestra el porcentaje, el tamaño escrito, la velocidad en MB/s y una estimación del tiempo restante. La barra de progreso utiliza el tamaño real que va teniendo el archivo de salida junto con el progreso temporal comunicado por FFmpeg.

La actualización de estadísticas de FFmpeg se mantiene en 0,25 segundos. Esto solo controla la frecuencia con la que se informa del progreso; no significa que el codificador realice una codificación nueva cada 0,25 segundos.

11. CONVERSIÓN DE VARIOS ARCHIVOS
Al pulsar «CONVERTIR», el programa procesa los MKV siguiendo exactamente el orden actual de la lista. Si has reorganizado la lista con ▲ y ▼, ese nuevo orden es el que se utilizará.

12. CARPETA DE SALIDA
La carpeta de salida se puede cambiar mediante «Examinar». Los archivos generados se guardan en la carpeta seleccionada siguiendo la lógica de nombres del programa.

13. LÍMITE DE TAMAÑO
El slider permite elegir desde 0 MB hasta 1 TB. El tamaño mostrado decide COPIA DIRECTA o NVENC directamente: si el vídeo cabe, se copia; si no cabe, se recodifica. Cuando se usa NVENC, el cálculo interno reserva 50 MB de margen de seguridad.

14. QUÉ NO CAMBIA AL REORDENAR
Mover archivos con ▲ o ▼ solamente cambia el orden de la cola. No cambia la configuración de calidad, el bitrate, el audio seleccionado, el procesamiento HDR ni el método de codificación.

RESUMEN
MK5 Converter está diseñado para convertir cualquier vídeo a MP4 con un tamaño objetivo configurable: si ya está dentro del tamaño elegido, intenta conservarlo mediante copia directa/lossless; si lo supera, lo transforma mediante HEVC NVENC P7, VBR, multipass fullres y Spatial AQ, buscando la máxima calidad posible dentro del tamaño efectivo. El modo Copy Direct no descuenta los 50 MB; esos 50 MB se reservan únicamente para el cálculo de NVENC. Mantiene la profundidad de bits adecuada y las características de color/HDR del origen cuando están disponibles. Finalmente procesa todos los archivos siguiendo el orden visible en la lista.

'@

    $infoText.Text = $infoContent.Trim()
    $infoText.Select(0,0)

    $btnDownloadCode.Add_Click({
        try {
            $saveDialog = New-Object System.Windows.Forms.SaveFileDialog
            $saveDialog.Title = 'Guardar código completo de MK5 Converter'
            $saveDialog.Filter = 'Archivo de texto (*.txt)|*.txt|Todos los archivos (*.*)|*.*'
            $saveDialog.DefaultExt = 'txt'
            $saveDialog.AddExtension = $true
            $saveDialog.FileName = 'MK5_Converter_codigo_completo.txt'
            $saveDialog.OverwritePrompt = $true

            if ($saveDialog.ShowDialog($script:InfoForm) -eq [System.Windows.Forms.DialogResult]::OK) {
                $batContent = [System.IO.File]::ReadAllText($bat, [System.Text.Encoding]::UTF8)
                $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
                [System.IO.File]::WriteAllText($saveDialog.FileName, $batContent, $utf8NoBom)
                # Ventana de confirmación personalizada para mostrar la ruta de forma limpia.
                $savedForm = New-Object System.Windows.Forms.Form
                $savedForm.Text = 'MK5 Converter'
                $savedForm.Size = New-Object System.Drawing.Size(560,235)
                $savedForm.MinimumSize = New-Object System.Drawing.Size(560,235)
                $savedForm.MaximumSize = New-Object System.Drawing.Size(560,235)
                $savedForm.StartPosition = 'CenterParent'
                $savedForm.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
                $savedForm.MaximizeBox = $false
                $savedForm.MinimizeBox = $false
                $savedForm.ShowInTaskbar = $false
                $savedForm.Font = New-Object System.Drawing.Font('Segoe UI',10)

                $iconBox = New-Object System.Windows.Forms.PictureBox
                $iconBox.Location = New-Object System.Drawing.Point(24,24)
                $iconBox.Size = New-Object System.Drawing.Size(42,42)
                $iconBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage
                $iconBox.Image = [System.Drawing.SystemIcons]::Information.ToBitmap()

                $titleLabel = New-Object System.Windows.Forms.Label
                $titleLabel.Location = New-Object System.Drawing.Point(82,22)
                $titleLabel.Size = New-Object System.Drawing.Size(440,32)
                $titleLabel.Font = New-Object System.Drawing.Font('Segoe UI Semibold',12)
                $titleLabel.Text = 'Código completo guardado correctamente'

                $pathLabel = New-Object System.Windows.Forms.Label
                $pathLabel.Location = New-Object System.Drawing.Point(82,62)
                $pathLabel.Size = New-Object System.Drawing.Size(440,24)
                $pathLabel.Text = 'Archivo guardado en:'

                $pathBox = New-Object System.Windows.Forms.Label
                $pathBox.Location = New-Object System.Drawing.Point(82,88)
                $pathBox.Size = New-Object System.Drawing.Size(440,38)
                $pathBox.AutoEllipsis = $true
                $pathBox.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft
                $pathBox.Text = [string]$saveDialog.FileName

                $folderButton = New-Object System.Windows.Forms.Button
                $folderButton.Text = 'Abrir código'
                $folderButton.Size = New-Object System.Drawing.Size(130,34)
                $folderButton.Location = New-Object System.Drawing.Point(276,145)
                $folderButton.FlatStyle = [System.Windows.Forms.FlatStyle]::System

                # Guardamos la ruta directamente en el boton para que el evento
                # funcione aunque el dialogo SaveFileDialog ya haya terminado.
                $folderButton.Tag = [string]$saveDialog.FileName

                # IMPORTANTE: estos eventos usan GetNewClosure() para conservar
                # exactamente este boton y esta ruta. Sin esto PowerShell puede
                # resolver $folderButton a otra variable/boton cuando el evento
                # se ejecuta mas tarde.
                $folderClickHandler = {
                    try {
                        $savedPath = [string]$this.Tag
                        if ([string]::IsNullOrWhiteSpace($savedPath)) { return }

                        if (-not (Test-Path -LiteralPath $savedPath -PathType Leaf)) {
                            [System.Windows.Forms.MessageBox]::Show(
                                ('El archivo no existe:`n`n{0}' -f $savedPath),
                                'MK5 Converter',
                                [System.Windows.Forms.MessageBoxButtons]::OK,
                                [System.Windows.Forms.MessageBoxIcon]::Warning
                            ) | Out-Null
                            return
                        }

                        # Abrimos directamente el archivo con la aplicacion asociada de Windows.
                        Start-Process -FilePath $savedPath -WindowStyle Normal | Out-Null

                        # Una vez abierto correctamente, cerramos esta ventana
                        # exactamente como si se hubiera pulsado Aceptar.
                        $savedForm.DialogResult = [System.Windows.Forms.DialogResult]::OK
                    } catch {
                        try {
                            [System.Windows.Forms.MessageBox]::Show(
                                ('No se pudo abrir el archivo:`n`n{0}' -f $_.Exception.Message),
                                'MK5 Converter',
                                [System.Windows.Forms.MessageBoxButtons]::OK,
                                [System.Windows.Forms.MessageBoxIcon]::Error
                            ) | Out-Null
                        } catch {}
                    }
                }.GetNewClosure()
                $folderButton.Add_Click($folderClickHandler)

                $folderKeyHandler = {
                    if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
                        $this.PerformClick()
                        $_.SuppressKeyPress = $true
                        $_.Handled = $true
                    }
                }.GetNewClosure()
                $folderButton.Add_KeyDown($folderKeyHandler)

                $okButton = New-Object System.Windows.Forms.Button
                $okButton.Text = 'Aceptar'
                $okButton.Size = New-Object System.Drawing.Size(100,34)
                $okButton.Location = New-Object System.Drawing.Point(422,145)
                $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
                $okButton.FlatStyle = [System.Windows.Forms.FlatStyle]::System

                # Aceptar queda enfocado al abrir el dialogo.
                # Enter activa el boton que tenga actualmente el foco.
                # Las flechas izquierda/derecha permiten cambiar el foco entre botones.

                [void]$savedForm.Controls.Add($iconBox)
                [void]$savedForm.Controls.Add($titleLabel)
                [void]$savedForm.Controls.Add($pathLabel)
                [void]$savedForm.Controls.Add($pathBox)
                [void]$savedForm.Controls.Add($folderButton)
                [void]$savedForm.Controls.Add($okButton)

                # Orden de teclado y foco inicial: Aceptar es el primer boton.
                $folderButton.TabIndex = 1
                $okButton.TabIndex = 0
                $savedForm.ActiveControl = $okButton
                $okButton.Focus() | Out-Null

                $savedForm.Add_KeyDown({
                    if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Left -or $_.KeyCode -eq [System.Windows.Forms.Keys]::Right) {
                        if ($savedForm.ActiveControl -eq $okButton) {
                            $folderButton.Focus() | Out-Null
                        } else {
                            $okButton.Focus() | Out-Null
                        }
                        $_.SuppressKeyPress = $true
                    }
                    elseif ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter -and $savedForm.ActiveControl -eq $folderButton) {
                        $savedForm.ActiveControl.PerformClick()
                        $_.SuppressKeyPress = $true
                        $_.Handled = $true
                    }
                })
                $savedForm.KeyPreview = $true

                [void]$savedForm.ShowDialog($script:InfoForm)
                $iconBox.Image.Dispose()
                $savedForm.Dispose()
            }
        }
        catch {
            [System.Windows.Forms.MessageBox]::Show(
                $script:InfoForm,
                ('No se pudo guardar el código:`n`n{0}' -f $_.Exception.Message),
                'MK5 Converter',
                [System.Windows.Forms.MessageBoxButtons]::OK,
                [System.Windows.Forms.MessageBoxIcon]::Error
            ) | Out-Null
        }
    })

    $script:InfoForm.Controls.Add($infoText)
    $script:InfoForm.Controls.Add($infoTopBar)
    $script:InfoForm.Add_FormClosed({ $script:InfoForm = $null })
    # Ventana independiente y no modal: puede usarse junto a la ventana principal.
    $script:InfoForm.Show() | Out-Null
    $script:InfoForm.BringToFront()
}

$btnInfo.Add_Click({ Show-ProgramInfo })

# Icono de carpeta dibujado para evitar problemas con emojis
# y, además, dejar una separación limpia entre el icono y el texto.
$folderIcon = New-Object System.Drawing.Bitmap(40,20)
$folderGraphics = [System.Drawing.Graphics]::FromImage($folderIcon)
$folderGraphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$folderGraphics.Clear([System.Drawing.Color]::Transparent)
$folderBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(245,205,95))
$folderPath = New-Object System.Drawing.Drawing2D.GraphicsPath
$folderPath.AddPolygon(@(
    (New-Object System.Drawing.Point(2,6)),
    (New-Object System.Drawing.Point(9,6)),
    (New-Object System.Drawing.Point(11,3)),
    (New-Object System.Drawing.Point(17,3)),
    (New-Object System.Drawing.Point(19,6)),
    (New-Object System.Drawing.Point(26,6)),
    (New-Object System.Drawing.Point(24,17)),
    (New-Object System.Drawing.Point(3,17))
))
$folderGraphics.FillPath($folderBrush,$folderPath)
$folderPath.Dispose()
$folderBrush.Dispose()
$folderGraphics.Dispose()

# Estilo del boton de carpeta: verde oscuro sobrio, geometrico y limpio.
$btnConvert.FlatAppearance.BorderSize = 1
$btnConvert.FlatAppearance.BorderColor = [System.Drawing.Color]::FromArgb(38,92,66)
$btnConvert.FlatAppearance.MouseOverBackColor = [System.Drawing.Color]::FromArgb(42,108,76)
$btnConvert.FlatAppearance.MouseDownBackColor = [System.Drawing.Color]::FromArgb(31,82,59)

# ============================================================
# DRAG & DROP
# ============================================================

function Add-MkvFiles {
    param(
        [string[]]$Paths
    )

    foreach ($path in $Paths) {

        if ([string]::IsNullOrWhiteSpace($path)) {
            continue
        }

        if (
            -not (
                Test-Path `
                    -LiteralPath $path `
                    -PathType Leaf
            )
        ) {
            continue
        }

        $extension =
            [System.IO.Path]::GetExtension($path)

        if ($extension.ToLower() -ne '.mkv') {
            continue
        }

        $exists =
            $script:Files |
            Where-Object {
                $_ -eq $path
            }

        if ($null -ne $exists) {
            continue
        }

        [void]$script:Files.Add($path)

        [void]$listFiles.Items.Add(
            [System.IO.Path]::GetFileName($path)
        )
    }

    if ($script:Files.Count -gt 0) {

        if ($listFiles.SelectedIndex -lt 0) {
            $listFiles.SelectedIndex = 0
        }
    }
}

$listFiles.Add_DragEnter({

    if (
        $_.Data.GetDataPresent(
            [System.Windows.Forms.DataFormats]::FileDrop
        )
    ) {

        $_.Effect =
            [System.Windows.Forms.DragDropEffects]::Copy
    }
})

$listFiles.Add_DragDrop({

    $files =
        $_.Data.GetData(
            [System.Windows.Forms.DataFormats]::FileDrop
        )

    Add-MkvFiles $files
})

# ============================================================
# CAMBIAR TAMAÑO OBJETIVO
# ============================================================

$trkTargetSize.Add_ValueChanged({
    if ($script:UpdatingTargetSize) { return }
    $script:UpdatingTargetSize = $true
    try {
        # Convertir la posicion REAL del slider mediante la nueva curva.
        $sliderMin = [double]$trkTargetSize.Minimum
        $sliderMax = [double]$trkTargetSize.Maximum
        $sliderSpan = $sliderMax - $sliderMin
        if ($sliderSpan -le 0.0) { $sliderSpan = 1.0 }

        $ratio = ([double]$trkTargetSize.Value - $sliderMin) / $sliderSpan
        if ($ratio -lt 0.0) { $ratio = 0.0 }
        if ($ratio -gt 1.0) { $ratio = 1.0 }

        $rawMB = Convert-SliderRatioToTargetMB $ratio
        $script:TargetSizeManualText = $null

        # Sin cuantizacion artificial: el slider usa directamente la curva.
        # El valor interno se ajusta a bytes, que es la precision maxima real.
        $script:TargetSizeMB = [double]$rawMB
        if ($script:TargetSizeMB -lt 0) { $script:TargetSizeMB = 0.0 }
        if ($script:TargetSizeMB -gt 1000000) { $script:TargetSizeMB = 1000000.0 }
        $script:TargetSizeBytes = [Int64]([math]::Round($script:TargetSizeMB * 1000000.0,0,[MidpointRounding]::AwayFromZero))
        $script:TargetSizeMB = $script:TargetSizeBytes / 1000000.0
        $effectiveMB = [math]::Max(0, $script:TargetSizeMB - 50)
        $script:LimitBytes = [Int64]($effectiveMB * 1000000)
        $lblTargetSizeValue.Text = Get-TargetSizeText

        if ($null -ne $script:CurrentFileData) {
            # El modo se decide directamente con el tamaño visible del slider.
            $script:SelectedFileCopyMode = ([Int64]$script:CurrentFileData.SizeBytes -le [Int64]$script:TargetSizeBytes)
            Update-ModeDisplay
            Update-AudioDetails
        }
    }
    finally {
        $script:UpdatingTargetSize = $false
    }
})

function Commit-TargetSizeEdit {
    $parsed = Parse-TargetSizeInput $txtTargetSizeEdit.Text $script:TargetSizeEditUnit
    if ($null -eq $parsed) {
        $txtTargetSizeEdit.Text = Get-TargetSizeText
        return
    }

    [void](Set-TargetSizeMB $parsed.MB -PreserveExact)

    # Mantener la unidad con la que el usuario empezó a editar. Si el valor
    # supera 1000 de esa unidad, subir automaticamente: MB -> GB -> TB -> PB -> EB.
    $unit = $parsed.Unit
    $bytes = [Int64]$script:TargetSizeBytes
    switch ($unit) {
        'MB' { if ($bytes -ge [Int64]1000000000) { $unit = 'GB' }; if ($bytes -ge [Int64]1000000000000) { $unit = 'TB' }; if ($bytes -ge [Int64]1000000000000000) { $unit = 'PB' }; if ($bytes -ge [Int64]1000000000000000000) { $unit = 'EB' } }
        'GB' { if ($bytes -ge [Int64]1000000000000) { $unit = 'TB' }; if ($bytes -ge [Int64]1000000000000000) { $unit = 'PB' }; if ($bytes -ge [Int64]1000000000000000000) { $unit = 'EB' } }
        'TB' { if ($bytes -ge [Int64]1000000000000000) { $unit = 'PB' }; if ($bytes -ge [Int64]1000000000000000000) { $unit = 'EB' } }
        'PB' { if ($bytes -ge [Int64]1000000000000000000) { $unit = 'EB' } }
    }

    # La unidad elegida se conserva y las comas quedan escritas despues de aceptar.
    $script:TargetSizeEditUnit = $unit
    $script:TargetSizeManualText = Format-GroupedTargetSizeText $script:TargetSizeBytes $unit
    $script:FormattingTargetSizeText = $true
    try {
        $txtTargetSizeEdit.Text = $script:TargetSizeManualText
        $txtTargetSizeEdit.SelectionStart = $txtTargetSizeEdit.Text.Length
    } finally {
        $script:FormattingTargetSizeText = $false
    }
}

$txtTargetSizeEdit.Add_KeyDown({
    if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) {
        Commit-TargetSizeEdit
        $_.SuppressKeyPress = $true
        $_.Handled = $true
    }
    elseif ($_.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
        $script:TargetSizeManualText = $null
        $txtTargetSizeEdit.Text = Get-TargetSizeText
        $_.SuppressKeyPress = $true
        $_.Handled = $true
    }
})

# Al hacer clic en cualquier otro control se acepta el valor, igual que Enter.
$script:CommitTargetSizeOnClick = {
    if ($this -ne $txtTargetSizeEdit -and $txtTargetSizeEdit.Focused) {
        Commit-TargetSizeEdit
    }
}.GetNewClosure()

function Register-TargetSizeCommitOnClick {
    param([System.Windows.Forms.Control]$Control)
    if ($Control -ne $txtTargetSizeEdit) {
        $Control.Add_MouseDown($script:CommitTargetSizeOnClick)
    }
    foreach ($child in $Control.Controls) {
        Register-TargetSizeCommitOnClick $child
    }
}

# ============================================================
# AÑADIR

$btnAdd.Add_Click({

    $dialog =
        New-Object System.Windows.Forms.OpenFileDialog

    $dialog.Title =
        'Seleccionar archivos MKV'

    $dialog.Filter =
        'Archivos MKV (*.mkv)|*.mkv'

    $dialog.Multiselect = $true

    if ($dialog.ShowDialog() -eq 'OK') {

        Add-MkvFiles $dialog.FileNames
    }
})

# ============================================================
# ELIMINAR
# ============================================================

$btnRemove.Add_Click({

    if ($script:Converting) {
        return
    }

    $index =
        $listFiles.SelectedIndex

    if ($index -ge 0) {

        $script:Files.RemoveAt($index)
        $listFiles.Items.RemoveAt($index)

        if ($script:Files.Count -gt 0) {

            if ($index -ge $script:Files.Count) {
                $index =
                    $script:Files.Count - 1
            }

            $listFiles.SelectedIndex =
                $index

        }
        else {

            $script:CurrentFileData = $null

            $comboAudio.Items.Clear()

            $lblAudioDetails.Text =
                'No hay audio seleccionado.'

            $lblFileInfo.Text =
                'Añade un archivo MKV para comenzar.'

            $lblMode.Text =
                'Modo: esperando archivo'

            $lblLimit.Visible = $false

            $progress.Value = 0
            $lblProgress.Text = '0%'
            $lblStatus.Text = 'Listo.'

            $script:ShowOutputFolder = $false

            $btnConvert.Image = $null

            $btnConvert.Text =
                '▶  CONVERTIR'

            $btnConvert.BackColor =
                [System.Drawing.Color]::FromArgb(
                    35,
                    145,
                    85
                )

            $btnConvert.FlatAppearance.MouseOverBackColor =
                [System.Drawing.Color]::FromArgb(42,108,76)
            $btnConvert.FlatAppearance.MouseDownBackColor =
                [System.Drawing.Color]::FromArgb(31,82,59)

            $btnConvert.Enabled = $true
        }
    }
})

# ============================================================
# MOVER ARCHIVOS EN EL ORDEN
# ============================================================

function Update-MoveButtons {

    if ($script:Converting -or $script:Reordering) {
        $btnMoveUp.Enabled = $false
        $btnMoveDown.Enabled = $false
        return
    }

    $count = $script:Files.Count
    if ($count -le 0 -or $listFiles.SelectedIndices.Count -eq 0) {
        $btnMoveUp.Enabled = $false
        $btnMoveDown.Enabled = $false
        return
    }

    $selected = New-Object 'System.Collections.Generic.HashSet[int]'
    foreach ($i in $listFiles.SelectedIndices) {
        [void]$selected.Add([int]$i)
    }

    $canMoveUp = $false
    for ($i = 1; $i -lt $count; $i++) {
        if ($selected.Contains($i) -and -not $selected.Contains($i - 1)) {
            $canMoveUp = $true
            break
        }
    }

    $canMoveDown = $false
    for ($i = 0; $i -lt ($count - 1); $i++) {
        if ($selected.Contains($i) -and -not $selected.Contains($i + 1)) {
            $canMoveDown = $true
            break
        }
    }

    $btnMoveUp.Enabled = $canMoveUp
    $btnMoveDown.Enabled = $canMoveDown
}

$btnMoveUp.Add_Click({

    if ($script:Converting -or $script:Reordering) {
        return
    }

    $count = $script:Files.Count
    if ($count -le 1 -or $listFiles.SelectedIndices.Count -eq 0) {
        return
    }

    # Copiamos orden y selección para poder hacer desplazamientos de un
    # paso sin perder la selección múltiple ni el orden interno de los bloques.
    $order = [System.Collections.Generic.List[string]]::new()
    $flags = [System.Collections.Generic.List[bool]]::new()
    foreach ($i in 0..($count - 1)) {
        [void]$order.Add([string]$script:Files[$i])
        [void]$flags.Add($listFiles.GetSelected($i))
    }

    # Un elemento seleccionado sube una posición si tiene delante uno no seleccionado.
    # Al recorrer de arriba abajo, los bloques seleccionados se mueven juntos.
    for ($i = 1; $i -lt $count; $i++) {
        if ($flags[$i] -and -not $flags[$i - 1]) {
            $tmp = $order[$i - 1]
            $order[$i - 1] = $order[$i]
            $order[$i] = $tmp

            $tmpFlag = $flags[$i - 1]
            $flags[$i - 1] = $flags[$i]
            $flags[$i] = $tmpFlag
        }
    }

    $script:Reordering = $true
    $listFiles.BeginUpdate()
    try {
        $script:Files.Clear()
        foreach ($path in $order) {
            [void]$script:Files.Add($path)
        }

        $listFiles.Items.Clear()
        foreach ($path in $script:Files) {
            [void]$listFiles.Items.Add([System.IO.Path]::GetFileName($path))
        }

        for ($i = 0; $i -lt $count; $i++) {
            if ($flags[$i]) {
                $listFiles.SetSelected($i,$true)
            }
        }
    }
    finally {
        $listFiles.EndUpdate()
        $script:Reordering = $false
    }

    $listFiles.Focus()
    Update-MoveButtons
})

$btnMoveDown.Add_Click({

    if ($script:Converting -or $script:Reordering) {
        return
    }

    $count = $script:Files.Count
    if ($count -le 1 -or $listFiles.SelectedIndices.Count -eq 0) {
        return
    }

    $order = [System.Collections.Generic.List[string]]::new()
    $flags = [System.Collections.Generic.List[bool]]::new()
    foreach ($i in 0..($count - 1)) {
        [void]$order.Add([string]$script:Files[$i])
        [void]$flags.Add($listFiles.GetSelected($i))
    }

    # Recorremos de abajo arriba para que los elementos seleccionados bajen
    # una posición sin adelantarse entre sí y manteniendo su orden interno.
    for ($i = $count - 2; $i -ge 0; $i--) {
        if ($flags[$i] -and -not $flags[$i + 1]) {
            $tmp = $order[$i + 1]
            $order[$i + 1] = $order[$i]
            $order[$i] = $tmp

            $tmpFlag = $flags[$i + 1]
            $flags[$i + 1] = $flags[$i]
            $flags[$i] = $tmpFlag
        }
    }

    $script:Reordering = $true
    $listFiles.BeginUpdate()
    try {
        $script:Files.Clear()
        foreach ($path in $order) {
            [void]$script:Files.Add($path)
        }

        $listFiles.Items.Clear()
        foreach ($path in $script:Files) {
            [void]$listFiles.Items.Add([System.IO.Path]::GetFileName($path))
        }

        for ($i = 0; $i -lt $count; $i++) {
            if ($flags[$i]) {
                $listFiles.SetSelected($i,$true)
            }
        }
    }
    finally {
        $listFiles.EndUpdate()
        $script:Reordering = $false
    }

    $listFiles.Focus()
    Update-MoveButtons
})

# ============================================================
# LIMPIAR
# ============================================================

$btnClear.Add_Click({

    if ($script:Converting) {
        return
    }

    $script:Files.Clear()
    $listFiles.Items.Clear()

    $script:CurrentFileData = $null

    $comboAudio.Items.Clear()
    $comboAudio.Enabled = $true

    $lblAudioDetails.Text =
        'No hay audio seleccionado.'

    $lblFileInfo.Text =
        'Añade un archivo MKV para comenzar.'

    $lblMode.Text =
        'Modo: esperando archivo'

    $progress.Value = 0
    $lblProgress.Text = '0%'
    $lblStatus.Text = 'Listo.'

    $script:ShowOutputFolder = $false

    $btnConvert.Image = $null

    $btnConvert.Text =
        '▶  CONVERTIR'

    $btnConvert.BackColor =
        [System.Drawing.Color]::FromArgb(
            35,
            145,
            85
        )

    $btnConvert.FlatAppearance.MouseOverBackColor =
        [System.Drawing.Color]::FromArgb(42,108,76)
    $btnConvert.FlatAppearance.MouseDownBackColor =
        [System.Drawing.Color]::FromArgb(31,82,59)

    $btnConvert.Enabled = $true
})

# ============================================================
# CARPETA SALIDA
# ============================================================

$btnBrowse.Add_Click({

    if ($script:Converting) {
        return
    }

    $dialog =
        New-Object System.Windows.Forms.FolderBrowserDialog

    $dialog.Description =
        'Selecciona la carpeta de salida'

    if (
        Test-Path `
            -LiteralPath $txtOutput.Text
    ) {

        $dialog.SelectedPath =
            $txtOutput.Text
    }

    if (
        $dialog.ShowDialog() -eq
        'OK'
    ) {

        $txtOutput.Text =
            $dialog.SelectedPath
    }
})

# ============================================================
# ACTUALIZAR INFORMACION
# ============================================================

function Refresh-CurrentFile {

    $index =
        $listFiles.SelectedIndex

    if ($index -lt 0) {
        return
    }

    if ($index -ge $script:Files.Count) {
        return
    }

    $file =
        $script:Files[$index]

    $script:CurrentFileIndex =
        $index

    $lblStatus.Text =
        'Analizando archivo...'

    $form.Refresh()

    try {

        $data =
            Analyze-File $file

        $script:CurrentFileData = $data

        $script:CurrentAudioTracks =
            @($data.AudioTracks)

        $script:CurrentSubtitleTracks =
            @($data.SubtitleTracks)

        $script:SelectedFileCopyMode = ($data.SizeBytes -le $script:TargetSizeBytes)

        # La indicación dorada cambia según el tamaño del archivo:
        # por encima del límite avisamos del límite; por debajo mostramos
        # el tamaño estimado de la salida.
        if ([Int64]$data.SizeBytes -gt [Int64]$script:TargetSizeBytes) {
            $lblLimit.Text = ('Tamaño límite de cálculo: {0}' -f (Get-EffectiveLimitText))
        }
        else {
            # En archivos por debajo del límite, la etiqueta mostrará el peso
            # estimado de salida cuando se actualice el modo y el audio.
            $lblLimit.Text = 'Calculando tamaño estimado...'
        }
        $lblLimit.Visible = $true

        if (
            $data.SizeBytes -le
            $script:TargetSizeBytes
        ) {

            $mode =
                'COPIA DIRECTA'
        }
        else {

            $mode =
                'NVENC HEVC'
        }

        $videoInfo =
            $data.VideoCodecText

        if ($data.VideoBitDepth) {
            $videoInfo +=
                ' | ' +
                $data.VideoBitDepth
        }

        if ($data.VideoProfile) {
            $videoInfo +=
                ' | ' +
                $data.VideoProfile
        }

        if ($data.VideoLevel) {
            $videoInfo +=
                ' | ' +
                $data.VideoLevel
        }

        if ($data.VideoHdr) {
            $videoInfo +=
                ' | ' +
                $data.VideoHdr
        }

        if ($data.VideoFps) {
            $videoInfo +=
                ' | ' +
                $data.VideoFps
        }

        if ($data.VideoVfr) {
            $videoInfo +=
                ' | ' +
                $data.VideoVfr
        }

        if ($data.VideoBitrate) {
            $videoInfo +=
                ' | Bitrate de vídeo: ' +
                $data.VideoBitrate
        }

        $resolutionText = if ($data.Width -and $data.Height) {
            'Resolución: {0}×{1} px' -f $data.Width,$data.Height
        } else {
            'Resolución desconocida'
        }

        $lblFileInfo.Text =
            (
                ('{0}' + [Environment]::NewLine + '{1}    |    Duración: {2}    |    Tamaño: {3}' + [Environment]::NewLine + 'Vídeo: {4}') -f
                $data.Name,
                $resolutionText,
                $data.DurationText,
                $data.SizeText,
                $videoInfo
            )

        $lblMode.Text =
            ('Modo: {0}' -f $mode)

        $comboAudio.Items.Clear()

        foreach ($track in $script:CurrentAudioTracks) {

            $defaultText = ''

            if ($track.Default) {
                $defaultText =
                    ' [DEFAULT]'
            }

            $originalText = ''

            if ($track.Original) {
                $originalText =
                    ' [ORIGINAL]'
            }

            $text =
                (
                    '{0}. {1} — {2} — {3} — {4} — {5} kbps — {6} kHz — {7}-bit{8}{9}' -f
                    $track.Number,
                    $track.Language,
                    $track.Title,
                    $track.CodecDisplay,
                    $track.ChannelText,
                    $track.Bitrate,
                    $track.SampleRateText,
                    $track.Bits,
                    $defaultText,
                    $originalText
                )

            [void]$comboAudio.Items.Add(
                $text
            )
        }

        if ($script:CurrentAudioTracks.Count -gt 0) {

            $comboAudio.SelectedIndex = 0

            if (
                $data.SizeBytes -le
                $script:TargetSizeBytes
            ) {

                for (
                    $audioIndex = 0;
                    $audioIndex -lt $script:CurrentAudioTracks.Count;
                    $audioIndex++
                ) {

                    $comboAudio.SetItemCheckState(
                        $audioIndex,
                        [System.Windows.Forms.CheckState]::Checked
                    )
                }

                $comboAudio.Enabled = $false
            }
            else {

                $comboAudio.Enabled = $true

                $comboAudio.SetItemCheckState(
                    0,
                    [System.Windows.Forms.CheckState]::Checked
                )
            }
        }
        else {

            [void]$comboAudio.Items.Add(
                'No hay pistas de audio'
            )

            $comboAudio.SelectedIndex = 0
        }

        Update-ModeDisplay

        $subtitleCompatible =
            @(
                $script:CurrentSubtitleTracks |
                Where-Object {
                    $_.Compatible
                }
            )

        $subtitleBitmap =
            @(
                $script:CurrentSubtitleTracks |
                Where-Object {
                    -not $_.Compatible
                }
            )

        $subtitleText = ''

        if (
            $script:CurrentSubtitleTracks.Count -eq 0
        ) {

            $subtitleText =
                'Sin subtítulos.'
        }
        else {

            $subtitleText =
                (
                    'Subtítulos: {0} de texto compatibles' -f
                    $subtitleCompatible.Count
                )

            if ($subtitleBitmap.Count -gt 0) {

                $subtitleText +=
                    (
                        ' | {0} PGS/bitmap/teletexto se descartarán' -f
                        $subtitleBitmap.Count
                    )
            }
        }

        $lblStatus.Text =
            $subtitleText

        Update-AudioDetails
    }
    catch {

        $lblStatus.Text =
            'Error analizando el archivo.'

        Show-Error $_.Exception.Message
    }
}

# ============================================================
# INFORMACION AUDIO
# ============================================================

function Update-ModeDisplay {
    if ($null -eq $script:CurrentFileData) { return }

    if ($script:SelectedFileCopyMode) {
        if (-not $script:Converting -and $script:CurrentAudioTracks.Count -gt 0) {
            for ($audioIndex = 0; $audioIndex -lt $script:CurrentAudioTracks.Count; $audioIndex++) {
                $comboAudio.SetItemCheckState($audioIndex, [System.Windows.Forms.CheckState]::Checked)
            }
            $comboAudio.Enabled = $false
        }
        $copyBitrateText = [string]$script:CurrentFileData.VideoBitrate
        if ([string]::IsNullOrWhiteSpace($copyBitrateText)) {
            $lblMode.Text = 'Modo: COPIA DIRECTA'
        } else {
            $lblMode.Text = 'Modo: COPIA DIRECTA    |    Bitrate de vídeo: {0}' -f $copyBitrateText
        }

        # En copia directa el tamaño exportado es, salvo metadatos del
        # contenedor, el mismo archivo original.
        if ([Int64]$script:CurrentFileData.SizeBytes -le [Int64]$script:TargetSizeBytes) {
            $lblLimit.Text = 'Tamaño estimado: {0}' -f $script:CurrentFileData.SizeText
            $lblLimit.Visible = $true
        }
        return
    }

    if (-not $script:Converting) { $comboAudio.Enabled = $true }

    $checked = @($comboAudio.CheckedIndices)
    $tracks = @()
    foreach ($i in $checked) {
        if ($i -ge 0 -and $i -lt $script:CurrentAudioTracks.Count) {
            $tracks += $script:CurrentAudioTracks[$i]
        }
    }

    if ($tracks.Count -eq 0 -and $script:CurrentAudioTracks.Count -gt 0) {
        $tracks = @($script:CurrentAudioTracks[0])
    }

    $audioKbps = 0
    foreach ($track in $tracks) {
        $audioKbps += Get-SelectedAudioBitrate $track
    }

    $targetVideoKbps = Get-VideoBitrate `
        -Duration $script:CurrentFileData.Duration `
        -AudioKbps $audioKbps
    $lblMode.Text = 'Modo: NVENC HEVC    |    Bitrate de vídeo: {0:N0} Kbps' -f $targetVideoKbps

    $estimatedOutputMB = (($targetVideoKbps + $audioKbps) * $script:CurrentFileData.Duration) / 8000
    if ($estimatedOutputMB -lt 0) { $estimatedOutputMB = 0 }
    $lblLimit.Text = 'Tamaño estimado: {0:N0} MB' -f $estimatedOutputMB
    $lblLimit.Visible = $true
}

function Update-AudioDetails {
    $checked = @($comboAudio.CheckedIndices)
    if ($checked.Count -eq 0) { $lblAudioDetails.Text = 'No hay audio seleccionado.'; return }

    $tracks = @()
    foreach ($i in $checked) {
        if ($i -ge 0 -and $i -lt $script:CurrentAudioTracks.Count) { $tracks += $script:CurrentAudioTracks[$i] }
    }
    if ($tracks.Count -eq 0) { $lblAudioDetails.Text = 'No hay audio seleccionado.'; return }

    if ($tracks.Count -eq 1) {
        $track = $tracks[0]
        $defaultText = if ($track.Default) { 'Sí' } else { 'No' }
        $originalText = if ($track.Original) { 'Sí' } else { 'No' }
        $sizeText = if ($track.Bytes -gt 0) { '{0:N2} MB' -f ($track.Bytes / 1000000) } else { '?' }
        $outputText = if ($script:SelectedFileCopyMode) {
            '→ AUDIO DE SALIDA: COPIA DIRECTA (sin recodificar)'
        } else {
            '→ AUDIO DE SALIDA: AAC'
        }
        $lblAudioDetails.Text = (
            "Codec: {0}    |    Canales: {1}    |    Bitrate de audio: {2} kbps    |    Sample rate: {3} kHz    |    Profundidad: {4}-bit`r`n" +
            "Duración: {5}    |    Tamaño: {6}    |    Default: {7}    |    Original: {8}    |    {9}"
        ) -f $track.CodecDisplay,$track.ChannelText,$track.Bitrate,$track.SampleRateText,$track.Bits,
            (Format-Duration $track.Duration),$sizeText,$defaultText,$originalText,$outputText
    } else {
        $names = @($tracks | ForEach-Object { $_.Language }) -join ', '
        [Int64]$totalAudioBytes = 0
        $sizes = @(
            $tracks | ForEach-Object {
                $trackBytes = [Int64]$_.Bytes
                if ($trackBytes -gt 0) { $totalAudioBytes += $trackBytes }
                $trackSizeText = if ($trackBytes -gt 0) { Format-Bytes $trackBytes } else { '?' }
                '{0}: {1}' -f $_.Language,$trackSizeText
            }
        ) -join '    |    '

        if ($script:SelectedFileCopyMode) {
            $outputText = 'Audio de salida: COPIA DIRECTA (sin recodificar)'
        } else {
            $outputText = 'Audio de salida: AAC'
        }

        $totalSizeText = if ($totalAudioBytes -gt 0) { Format-Bytes $totalAudioBytes } else { '?' }
        $lblAudioDetails.Text = (
            "Pistas seleccionadas: {0}    |    Idiomas: {1}`r`nTamaño por pista: {2}    |    Total audio: {3}    |    {4}"
        ) -f $tracks.Count,$names,$sizes,$totalSizeText,$outputText
    }
}
$comboAudio.Add_ItemCheck({

    $form.BeginInvoke(
        [Action]{
            Update-AudioDetails
            Update-ModeDisplay
        }
    ) | Out-Null
})

$listFiles.Add_SelectedIndexChanged({

    if (-not $script:Converting -and -not $script:Reordering) {
        Refresh-CurrentFile
    }

    Update-MoveButtons
})

Update-MoveButtons

# ============================================================
# ACTUALIZAR PROGRESO
# ============================================================

function Update-ProgressUi {
    if (-not $script:Converting) { return }

    try {
        $outputFile = [string]$script:CurrentOutputFile
        $inputBytes = [Int64]$script:CurrentInputBytes
        if ([string]::IsNullOrWhiteSpace($outputFile) -or $inputBytes -le 0 -or -not (Test-Path -LiteralPath $outputFile)) {
            if ($progress.Value -lt 3) { $progress.Value = [math]::Min(3,$progress.Value + 1); $lblProgress.Text = ('{0}%' -f $progress.Value) }
            return
        }

        $currentBytes = [Int64](Get-Item -LiteralPath $outputFile).Length
        $percent = [int][math]::Floor(($currentBytes / $inputBytes) * 100)
        if ($percent -lt 0) { $percent = 0 }; if ($percent -gt 98) { $percent = 98 }
        $progress.Value = $percent
        $lblProgress.Text = ('{0}%' -f $percent)

        $now = [DateTime]::UtcNow
        if ($null -eq $script:OutputSampleAt) {
            $script:OutputSampleAt = $now
            $script:OutputSampleBytes = $currentBytes
        }
        $elapsed = ($now - $script:OutputSampleAt).TotalSeconds
        if ($elapsed -ge 1.50) {
            $written = [math]::Max(0,($currentBytes - [Int64]$script:OutputSampleBytes))
            $instantBps = $written / $elapsed
            if ($script:OutputBytesPerSecond -gt 0) {
                $script:OutputBytesPerSecond = ($script:OutputBytesPerSecond * 0.80) + ($instantBps * 0.20)
            } else {
                $script:OutputBytesPerSecond = $instantBps
            }
            $script:OutputSampleAt = $now
            $script:OutputSampleBytes = $currentBytes
        }

        if ($script:CurrentCopyMode) {
            $lblStatus.Text = ('Copiando sin recodificar... {0}' -f (Format-Bytes $currentBytes))
            return
        }

        $speedText = if ($script:OutputBytesPerSecond -gt 0) {
            '{0:N2} MB/s' -f ($script:OutputBytesPerSecond / 1MB)
        } else {
            'calculando velocidad...'
        }

        # ETA estilo Windows: se calcula con la misma velocidad media suavizada
        # que se muestra en MB/s. La velocidad solo cambia cada 1,5 s y cada
        # nueva medición se mezcla con la anterior, evitando saltos bruscos.
        $etaText = 'Calculando tiempo restante...'

        if ($script:OutputBytesPerSecond -gt 0 -and $currentBytes -gt 0) {
            $estimatedFinalBytes = [double]$LimitBytes

            $duration = [double]$script:FFmpegProgressDuration
            $mediaSeconds = [double]$script:FFmpegProgressSeconds

            if ($duration -gt 0 -and $mediaSeconds -gt 0) {
                $fraction = $mediaSeconds / $duration

                if ($fraction -ge 0.02 -and $fraction -le 1.0) {
                    $estimatedFinalBytes = [double]$currentBytes / $fraction

                    if ($estimatedFinalBytes -gt [double]$LimitBytes) {
                        $estimatedFinalBytes = [double]$LimitBytes
                    }
                }
            }

            $remainingBytes = [math]::Max(0,($estimatedFinalBytes - [double]$currentBytes))
            $remainingSeconds = $remainingBytes / [double]$script:OutputBytesPerSecond

            if ($remainingSeconds -lt 1) {
                $remainingSeconds = 1
            }

            $etaText = 'Tiempo restante: {0}' -f (Format-Duration $remainingSeconds)
        }

        $lblStatus.Text = ('Codificando NVENC... {0}    |    {1}    |    {2}' -f (Format-Bytes $currentBytes),$speedText,$etaText)
    }
    catch {}
}
# ============================================================
# EJECUTAR FFMPEG
# ============================================================

function Start-FFmpegConversion {

    param(
        [string]$InputFile,
        [string]$OutputFile,
        [array]$AudioTracks,
        [int]$AudioBitrate,
        [int]$VideoBitrate,
        [bool]$CopyMode,
        [array]$SubtitleTracks,
        [double]$Duration = 0,
        $VideoStream = $null
    )

    $arguments =
        New-Object System.Collections.Generic.List[string]

    [void]$arguments.Add(
        '-hide_banner'
    )

    [void]$arguments.Add(
        '-y'
    )

    [void]$arguments.Add(
        '-i'
    )

    [void]$arguments.Add(
        '"' +
        $InputFile.Replace('"','\"') +
        '"'
    )

    [void]$arguments.Add(
        '-map_chapters'
    )

    [void]$arguments.Add(
        '0'
    )

    # ========================================================
    # VIDEO
    # ========================================================

    [void]$arguments.Add(
        '-map'
    )

    [void]$arguments.Add(
        '0:v'
    )

    # ========================================================
    # AUDIO
    # ========================================================

    foreach ($audio in $AudioTracks) {

        [void]$arguments.Add(
            '-map'
        )

        [void]$arguments.Add(
            "0:$($audio.Index)"
        )
    }

    # ========================================================
    # SUBTITULOS
    # ========================================================

    foreach ($sub in $SubtitleTracks) {

        if ($sub.Compatible) {

            [void]$arguments.Add(
                '-map'
            )

            [void]$arguments.Add(
                "0:$($sub.Index)?"
            )
        }
    }

    # ========================================================
    # CODECS
    # ========================================================

    if ($CopyMode) {

        [void]$arguments.Add(
            '-c:v'
        )

        [void]$arguments.Add(
            'copy'
        )

        [void]$arguments.Add(
            '-c:a'
        )

        [void]$arguments.Add(
            'copy'
        )
    }
    else {

        [void]$arguments.Add(
            '-c:v'
        )

        [void]$arguments.Add(
            'hevc_nvenc'
        )

        [void]$arguments.Add(
            '-b:v'
        )

        [void]$arguments.Add(
            "${VideoBitrate}k"
        )

        [void]$arguments.Add(
            '-preset'
        )

        [void]$arguments.Add(
            'p7'
        )

        # P7 ya es el preset de máxima calidad de NVENC. Estas opciones
        # completan el perfil de calidad para un uso de archivo/almacenamiento:
        # HQ explícito, VBR explícito, doble pasada a resolución completa y
        # cuantización adaptativa espacial. No cambian el bitrate objetivo.
        [void]$arguments.Add('-tune')
        [void]$arguments.Add('hq')
        [void]$arguments.Add('-rc')
        [void]$arguments.Add('vbr')
        [void]$arguments.Add('-multipass')
        [void]$arguments.Add('fullres')
        [void]$arguments.Add('-spatial-aq')
        [void]$arguments.Add('1')
        [void]$arguments.Add('-aq-strength')
        [void]$arguments.Add('10')

        [void]$arguments.Add(
            '-pix_fmt'
        )

        # 8-bit se mantiene en 8-bit; 10-bit se mantiene en 10-bit;
        # 12-bit se reduce a 10-bit porque HEVC NVENC no ofrece salida 12-bit.
        if ([string]$script:CurrentVideoBitDepth -eq '10-bit' -or [string]$script:CurrentVideoBitDepth -eq '12-bit') {
            [void]$arguments.Add('p010le')
        }
        else {
            [void]$arguments.Add('yuv420p')
        }

        # HEVC Main 10 para 10/12-bit (12-bit de entrada se reduce a 10-bit);
        # Main para 8-bit. Evita depender de una selección implícita del perfil.
        [void]$arguments.Add('-profile:v')
        if ([string]$script:CurrentVideoBitDepth -eq '10-bit' -or [string]$script:CurrentVideoBitDepth -eq '12-bit') {
            [void]$arguments.Add('main10')
        }
        else {
            [void]$arguments.Add('main')
        }

        [void]$arguments.Add(
            '-tag:v'
        )

        [void]$arguments.Add(
            'hvc1'
        )

        # Conservar explícitamente las características de color declaradas
        # por el vídeo de origen. Se reutiliza el mismo stream que FFprobe ya
        # obtuvo durante Analyze-File: no se vuelve a analizar el archivo.
        try {
            foreach ($pair in @(
                @('-color_primaries', [string]$VideoStream.color_primaries),
                @('-color_trc',        [string]$VideoStream.color_transfer),
                @('-colorspace',       [string]$VideoStream.color_space),
                @('-color_range',      [string]$VideoStream.color_range)
            )) {
                if (-not [string]::IsNullOrWhiteSpace($pair[1]) -and $pair[1] -ne 'unknown') {
                    [void]$arguments.Add($pair[0])
                    [void]$arguments.Add($pair[1])
                }
            }
        } catch {}

        # Copiar los metadatos del contenedor de origen. FFmpeg/NVENC puede
        # transportar además el side data HDR disponible durante la decodificación
        # (por ejemplo, mastering display y MaxCLL/MaxFALL cuando está presente).
        [void]$arguments.Add('-map_metadata')
        [void]$arguments.Add('0')

        [void]$arguments.Add(
            '-c:a'
        )

        [void]$arguments.Add(
            'aac'
        )

        [void]$arguments.Add(
            '-b:a'
        )

        [void]$arguments.Add(
            "${AudioBitrate}k"
        )
    }

    # ========================================================
    # SUBTITULOS
    # ========================================================

    if ($SubtitleTracks.Count -gt 0) {

        [void]$arguments.Add(
            '-c:s'
        )

        [void]$arguments.Add(
            'mov_text'
        )
    }
    else {

        [void]$arguments.Add(
            '-sn'
        )
    }

    # ========================================================
    # PROGRESO FFMPEG
    #
    # Se usa un archivo temporal, no pipe:1. De este modo no hay
    # callbacks DataReceived que puedan cerrar PowerShell y dejar
    # FFmpeg convirtiendo sin interfaz.
    # ========================================================

    $progressFile = Join-Path ([System.IO.Path]::GetTempPath()) (
        'MKVConverter-' + [Guid]::NewGuid().ToString('N') + '.progress'
    )

    [void]$arguments.Add('-progress')
    [void]$arguments.Add('"' + $progressFile.Replace('"','\"') + '"')
    [void]$arguments.Add('-nostats')
    [void]$arguments.Add('-stats_period')
    [void]$arguments.Add('0.25')
    # ========================================================
    # ARCHIVO SALIDA
    # ========================================================

    [void]$arguments.Add(
        '"' +
        $OutputFile.Replace('"','\"') +
        '"'
    )

    $argString =
        $arguments -join ' '

    $psi =
        New-Object System.Diagnostics.ProcessStartInfo

    $psi.FileName =
        $FFmpeg

    $psi.Arguments =
        $argString

    $psi.UseShellExecute =
        $false

    $psi.CreateNoWindow =
        $true

    $psi.RedirectStandardOutput =
        $false

    $psi.RedirectStandardError =
        $false

    $process =
        New-Object System.Diagnostics.Process

    $process.StartInfo =
        $psi

    # Conservamos una referencia para no dejar FFmpeg huérfano si se cierra la ventana.
    $script:ActiveFfmpegProcess = $process

    $script:FFmpegLastError =
        ''

    $script:FFmpegProgressSeconds =
        0.0

    $script:FFmpegProgressDuration =
        $Duration

    $script:FFmpegSpeed = 0.0

    $script:CurrentCopyMode =
        $CopyMode

    $script:CurrentOutputFile =
        $OutputFile

    $script:OutputSampleAt = $null
    $script:OutputSampleBytes = 0
    $script:OutputBytesPerSecond = 0.0

    try {

        $script:CurrentInputBytes =
            [Int64](
                Get-Item `
                    -LiteralPath $InputFile
            ).Length
    }
    catch {

        $script:CurrentInputBytes =
            0
    }

    # ========================================================
    # INICIAR Y VIGILAR FFMPEG
    # ========================================================

    try {
        [void]$process.Start()
    }
    catch {
        throw ('No se pudo iniciar FFmpeg.`r`n`r`n' + $_.Exception.Message)
    }

    try {
        while (-not $process.HasExited) {
            [System.Windows.Forms.Application]::DoEvents()

            # Leer este archivo es seguro aunque FFmpeg lo esté escribiendo.
            # Si justo está bloqueado, se ignora ese ciclo y se vuelve a leer.
            try {
                if (Test-Path -LiteralPath $progressFile) {
                    $progressText = [System.IO.File]::ReadAllText(
                        $progressFile,
                        [System.Text.Encoding]::ASCII
                    )

                    if ($progressText -match '(?m)^out_time_ms=(\d+)\s*$') {
                        [double]$timeUs = 0
                        if ([double]::TryParse(
                            $Matches[1],
                            [Globalization.NumberStyles]::Float,
                            [Globalization.CultureInfo]::InvariantCulture,
                            [ref]$timeUs
                        )) {
                            $script:FFmpegProgressSeconds = $timeUs / 1000000
                        }
                    }

                    if ($progressText -match '(?m)^speed=([0-9]+(?:\.[0-9]+)?)x\s*$') {
                        [double]$ffmpegSpeed = 0
                        if ([double]::TryParse(
                            $Matches[1],
                            [Globalization.NumberStyles]::Float,
                            [Globalization.CultureInfo]::InvariantCulture,
                            [ref]$ffmpegSpeed
                        )) {
                            if ($ffmpegSpeed -gt 0) {
                                $script:FFmpegSpeed = $ffmpegSpeed
                            }
                        }
                    }
                }
            }
            catch {}

            Update-ProgressUi

            if ($script:CancelRequested) {
                try { $process.Kill() } catch {}
                throw 'Conversión cancelada por el usuario.'
            }

            Start-Sleep -Milliseconds 80
        }

        try { $process.WaitForExit() } catch {}

        if ($script:CancelRequested) {
            throw 'Conversión cancelada por el usuario.'
        }
    }
    finally {
        $script:ActiveFfmpegProcess = $null
        try {
            if (Test-Path -LiteralPath $progressFile) {
                Remove-Item -LiteralPath $progressFile -Force -ErrorAction Stop
            }
        }
        catch {}
    }
    # ========================================================
    # COMPROBAR RESULTADO
    # ========================================================

    $exitCode =
        $process.ExitCode

    if ($exitCode -ne 0) {

        $lastError =
            $script:FFmpegLastError.Trim()

        if (
            [string]::IsNullOrWhiteSpace(
                $lastError
            )
        ) {

            $lastError =
                'FFmpeg no proporcionó información adicional.'
        }

        throw (
            "FFmpeg terminó con código $exitCode.`r`n`r`n" +
            $lastError
        )
    }

    if (
        -not (
            Test-Path `
                -LiteralPath $OutputFile
        )
    ) {

        throw (
            'FFmpeg terminó correctamente pero no se encontró ' +
            'el MP4 de salida.'
        )
    }

    # ========================================================
    # FINAL
    # ========================================================

    $progress.Value =
        100

    $lblProgress.Text =
        '100%'

    $lblStatus.Text =
        'Finalizando archivo...'

    $form.Refresh()

    Start-Sleep `
        -Milliseconds 100

    [System.Windows.Forms.Application]::DoEvents()
}

# ============================================================
# CONVERTIR
# ============================================================

$btnConvert.Add_Click({
    if ($script:Converting) {
        if (-not $script:CancelRequested) {
            $script:CancelRequested = $true
            $btnConvert.Enabled = $false
            $btnConvert.Text = '⏳  ABORTANDO...'
            $lblStatus.Text = 'Abortando conversión y eliminando el MP4 parcial...'
            $form.Refresh()
            try {
                if ($null -ne $script:ActiveFfmpegProcess -and -not $script:ActiveFfmpegProcess.HasExited) { $script:ActiveFfmpegProcess.Kill() }
            }
            catch {}
        }
        return
    }

    # --------------------------------------------------------
    # SI YA TERMINO, EL BOTON SOLO ABRE LA CARPETA
    # --------------------------------------------------------

    if ($script:ShowOutputFolder) {

        $folderToOpen =
            $txtOutput.Text.Trim()

        if (
            Test-Path `
                -LiteralPath $folderToOpen
        ) {

            Start-Process `
                explorer.exe `
                -ArgumentList (
                    '"' +
                    $folderToOpen +
                    '"'
                )
        }

        return
    }

    # --------------------------------------------------------
    # EVITAR DOBLE CONVERSION
    # --------------------------------------------------------

    if ($script:Converting) {
        return
    }

    # --------------------------------------------------------
    # COMPROBAR ARCHIVOS
    # --------------------------------------------------------

    if ($script:Files.Count -eq 0) {

        Show-Info `
            'Añade al menos un archivo MKV.'

        return
    }

    $outputDirectory =
        $txtOutput.Text.Trim()

    if (
        [string]::IsNullOrWhiteSpace(
            $outputDirectory
        )
    ) {

        Show-Error `
            'Selecciona una carpeta de salida.'

        return
    }

    if (
        -not (
            Test-Path `
                -LiteralPath $outputDirectory
        )
    ) {

        Show-Error `
            'La carpeta de salida no existe.'

        return
    }

    # ========================================================
    # INICIAR
    # ========================================================

    $script:Converting =
        $true

    $script:CancelRequested =
        $false

    $script:ShowOutputFolder =
        $false

    $script:FFmpegProgressSeconds =
        0.0

    $script:FFmpegProgressDuration =
        0.0
    $script:FFmpegSpeed = 0.0

    $script:CurrentOutputFile =
        ''

    $script:CurrentInputBytes =
        0

    $script:CurrentCopyMode =
        $false

    $btnConvert.Enabled =
        $false

    $btnConvert.Text =
        '⏳  CONVIRTIENDO...'

    $btnConvert.Image = $null

    $btnConvert.BackColor =
        [System.Drawing.Color]::FromArgb(
            100,
            100,
            45
        )

    # Restaurar el hover verde del boton CONVERTIR
    # para que al iniciar una nueva conversion no herede
    # el hover azul del boton MOSTRAR CARPETA.
    $btnConvert.FlatAppearance.MouseOverBackColor =
        [System.Drawing.Color]::FromArgb(42,108,76)
    $btnConvert.FlatAppearance.MouseDownBackColor =
        [System.Drawing.Color]::FromArgb(31,82,59)

    $btnConvert.Image = $null
    $btnConvert.ImageAlign = [System.Drawing.ContentAlignment]::MiddleCenter
    $btnConvert.TextAlign = [System.Drawing.ContentAlignment]::MiddleCenter
    $btnConvert.TextImageRelation = [System.Windows.Forms.TextImageRelation]::Overlay
    $btnConvert.Padding = New-Object System.Windows.Forms.Padding(0)

    $btnConvert.Enabled = $true
    $btnConvert.Text = '■  ABORTAR MISIÓN'
    $btnConvert.BackColor = [System.Drawing.Color]::FromArgb(175,45,45)
    # Mismo nivel proporcional de oscurecimiento que el hover verde.
    $btnConvert.FlatAppearance.MouseOverBackColor = [System.Drawing.Color]::FromArgb(145,37,37)
    $btnConvert.FlatAppearance.MouseDownBackColor = [System.Drawing.Color]::FromArgb(145,37,37)
    $btnAdd.Enabled =
        $false

    # Durante la conversión no se permite cambiar la carpeta de salida
    # ni escribir manualmente una ruta.
    $btnBrowse.Enabled = $false
    $txtOutput.Enabled = $false

    $btnRemove.Enabled =
        $false

    $btnClear.Enabled =
        $false

    # Durante la conversión no se permite modificar las pistas de audio ni el tamaño objetivo.
    $comboAudio.Enabled = $false
    $trkTargetSize.Enabled = $false
    $lblTargetSizeValue.Enabled = $false

    try {

        # ====================================================
        # PROCESAR TODOS LOS ARCHIVOS
        # ====================================================

        for (
            $fileIndex = 0;
            $fileIndex -lt $script:Files.Count;
            $fileIndex++
        ) {

            if ($script:CancelRequested) {

                throw (
                    'Conversión cancelada por el usuario.'
                )
            }

            $inputFile =
                $script:Files[$fileIndex]

            $listFiles.SelectedIndex =
                $fileIndex

            [System.Windows.Forms.Application]::DoEvents()

            $data =
                Analyze-File $inputFile

            $audioTracks =
                @($data.AudioTracks)

            $subtitleTracks =
                @(
                    $data.SubtitleTracks |
                    Where-Object {
                        $_.Compatible
                    }
                )

            if ($audioTracks.Count -eq 0) {

                Show-Error (
                    "El archivo no contiene pistas de audio:`r`n`r`n" +
                    $data.Name
                )

                continue
            }

            # =================================================
            # DETERMINAR MODO
            # =================================================

            $copyMode =
                (
                    $data.SizeBytes -le
                    $script:TargetSizeBytes
                )

            # =================================================
            # MODO COPIA DIRECTA
            # =================================================

            if ($copyMode) {

                # TODOS LOS AUDIOS
                $selectedAudios =
                    @($audioTracks)

                $audioBitrate = 0

                foreach (
                    $selectedAudio in
                    $selectedAudios
                ) {

                    $audioBitrate +=
                        Get-SelectedAudioBitrate `
                            $selectedAudio
                }

                $videoBitrate = 0

                $copyBitrateText = [string]$data.VideoBitrate
                if ([string]::IsNullOrWhiteSpace($copyBitrateText)) {
                    $lblMode.Text = 'Modo: COPIA DIRECTA'
                } else {
                    $lblMode.Text = 'Modo: COPIA DIRECTA    |    Bitrate de vídeo: {0}' -f $copyBitrateText
                }

                if ([Int64]$data.SizeBytes -le [Int64]$script:TargetSizeBytes) {
                    $lblLimit.Text = 'Tamaño estimado: {0}' -f $data.SizeText
                    $lblLimit.Visible = $true
                }

                $lblStatus.Text =
                    ('Archivo ≤ {0} bytes. Copiando vídeo y TODOS los audios...' -f $script:TargetSizeBytes)

                $form.Refresh()
            }

            # =================================================
            # MODO NVENC
            # =================================================

            else {

                $selectedAudioIndices =
                    @(
                        $comboAudio.CheckedIndices
                    )

                $selectedAudios =
                    @()

                foreach (
                    $idx in
                    $selectedAudioIndices
                ) {

                    if (
                        $idx -ge 0 -and
                        $idx -lt $audioTracks.Count
                    ) {

                        $selectedAudios +=
                            $audioTracks[$idx]
                    }
                }

                if ($selectedAudios.Count -eq 0) {

                    $selectedAudios =
                        @($audioTracks[0])

                    if (
                        $fileIndex -eq
                        $listFiles.SelectedIndex
                    ) {

                        $comboAudio.SetItemCheckState(
                            0,
                            [System.Windows.Forms.CheckState]::Checked
                        )
                    }
                }

                $audioBitrate = 0

                foreach (
                    $selectedAudio in
                    $selectedAudios
                ) {

                    $audioBitrate +=
                        Get-SelectedAudioBitrate `
                            $selectedAudio
                }

                $videoBitrate =
                    Get-VideoBitrate `
                        -Duration $data.Duration `
                        -AudioKbps $audioBitrate

                $lblMode.Text =
                    (
                        'Modo: NVENC HEVC    |    Bitrate de vídeo: {0:N0} Kbps' -f
                        $videoBitrate
                    )

                $estimatedOutputMB = (($videoBitrate + $audioBitrate) * $data.Duration) / 8000
                if ($estimatedOutputMB -lt 0) { $estimatedOutputMB = 0 }
                $lblLimit.Text = 'Tamaño estimado: {0:N0} MB' -f $estimatedOutputMB
                $lblLimit.Visible = $true

                $lblStatus.Text =
                    'Preparando codificación NVIDIA NVENC...'

                $form.Refresh()
            }

            # =================================================
            # GENERAR SALIDA
            # =================================================

            $outputFile =
                Get-SafeOutputPath `
                    -Directory $outputDirectory `
                    -BaseName $data.BaseName

            $script:CurrentOutputFile =
                $outputFile

            $script:CurrentInputBytes =
                $data.SizeBytes

            $script:CurrentCopyMode =
                $copyMode

            $progress.Value =
                0

            $lblProgress.Text =
                '0%'

            $lblStatus.Text =
                (
                    'Convirtiendo: {0}' -f
                    $data.Name
                )

            $form.Refresh()

            # =================================================
            # EJECUTAR
            # =================================================

            $script:ActiveOutputFile = $outputFile
            $script:CurrentVideoBitDepth = [string]$data.VideoBitDepth

            Start-FFmpegConversion `
                -InputFile $inputFile `
                -OutputFile $outputFile `
                -AudioTracks $selectedAudios `
                -AudioBitrate $audioBitrate `
                -VideoBitrate $videoBitrate `
                -CopyMode $copyMode `
                -SubtitleTracks $subtitleTracks `
                -Duration $data.Duration `
                -VideoStream $data.VideoStream

            # =================================================
            $script:ActiveOutputFile = $null

            # RESULTADO
            # =================================================

            $finalFile =
                Get-Item `
                    -LiteralPath $outputFile

            $sizeFinal =
                Format-Bytes $finalFile.Length

            $progress.Value =
                100

            $lblProgress.Text =
                '100%'

            $lblStatus.Text =
                (
                    '✓ LISTO: {0}    |    {1}' -f
                    $finalFile.Name,
                    $sizeFinal
                )

            $form.Refresh()

            Start-Sleep `
                -Milliseconds 300

            [System.Windows.Forms.Application]::DoEvents()
        }

        # ====================================================
        # TODO TERMINADO
        #
        # IMPORTANTE:
        # NO se cierra el programa.
        # ====================================================

        $progress.Value =
            100

        $lblProgress.Text =
            '100%'

        $script:ShowOutputFolder =
            $true

        $lblMode.Text =
            '✓ LISTO — conversión terminada'

        $lblStatus.Text =
            'Conversión terminada. El MP4 está listo en la carpeta de salida.'

        $btnConvert.Text =
            'MOSTRAR CARPETA'

        $btnConvert.Image = $folderIcon
        $btnConvert.ImageAlign =
            [System.Drawing.ContentAlignment]::MiddleLeft
        $btnConvert.TextAlign =
            [System.Drawing.ContentAlignment]::MiddleCenter
        $btnConvert.TextImageRelation =
            [System.Windows.Forms.TextImageRelation]::ImageBeforeText
        $btnConvert.Padding =
            New-Object System.Windows.Forms.Padding(10,0,10,0)

        $btnConvert.FlatAppearance.BorderSize = 1
        $btnConvert.FlatAppearance.BorderColor =
            [System.Drawing.Color]::FromArgb(74,112,140)
        $btnConvert.FlatAppearance.MouseOverBackColor =
            [System.Drawing.Color]::FromArgb(132,170,198)
        $btnConvert.FlatAppearance.MouseDownBackColor =
            [System.Drawing.Color]::FromArgb(82,119,148)

        $btnConvert.BackColor =
            [System.Drawing.Color]::FromArgb(104,140,166)

        $btnConvert.Enabled =
            $true

        $form.Refresh()

        [System.Windows.Forms.Application]::DoEvents()

    }
    catch {
        if ($_.Exception.Message -eq 'Conversión cancelada por el usuario.') {
            $partialFile = [string]$script:ActiveOutputFile
            $removed = $true
            if (-not [string]::IsNullOrWhiteSpace($partialFile)) {
                for ($attempt = 1; $attempt -le 20; $attempt++) {
                    try { if (Test-Path -LiteralPath $partialFile) { Remove-Item -LiteralPath $partialFile -Force -ErrorAction Stop } } catch {}
                    if (-not (Test-Path -LiteralPath $partialFile)) { break }
                    Start-Sleep -Milliseconds 100
                }
                $removed = -not (Test-Path -LiteralPath $partialFile)
            }
            $script:ActiveOutputFile = $null
            $lblStatus.Text = 'Conversión abortada.'
            $progress.Value = 0
            $lblProgress.Text = '0%'
            if ($removed) {
                Show-Info "Conversión abortada.`r`n`r`nSe ha verificado que el MP4 parcial se ha eliminado."
            } else {
                Show-Error ("La conversión se abortó, pero no se pudo eliminar el archivo parcial:`r`n`r`n" + $partialFile)
            }
        } else {
            $lblStatus.Text = 'Se produjo un error.'
            Show-Error $_.Exception.Message
        }
    }    finally {

        $script:Converting =
            $false

        $btnAdd.Enabled =
            $true

        # Volver a permitir cambiar la carpeta de salida al terminar.
        $btnBrowse.Enabled = $true
        $txtOutput.Enabled = $true

        $btnRemove.Enabled =
            $true

        $btnClear.Enabled =
            $true

        $trkTargetSize.Enabled = $true
        $lblTargetSizeValue.Enabled = $true

        if ($null -ne $script:CurrentFileData) {
            if ($script:SelectedFileCopyMode) {
                $comboAudio.Enabled = $false
            } else {
                $comboAudio.Enabled = $true
            }
        } else {
            $comboAudio.Enabled = $true
        }

        # ----------------------------------------------------
        # SI TERMINO CORRECTAMENTE:
        # dejamos MOSTRAR CARPETA.
        #
        # SI HUBO ERROR/CANCELACION:
        # volvemos a CONVERTIR.
        # ----------------------------------------------------

        if (-not $script:ShowOutputFolder) {

            $btnConvert.Enabled =
                $true

            $btnConvert.Image = $null

            $btnConvert.Text =
                '▶  CONVERTIR'

            $btnConvert.BackColor =
                [System.Drawing.Color]::FromArgb(
                    35,
                    145,
                    85
                )

            $btnConvert.FlatAppearance.MouseOverBackColor =
                [System.Drawing.Color]::FromArgb(42,108,76)
            $btnConvert.FlatAppearance.MouseDownBackColor =
                [System.Drawing.Color]::FromArgb(31,82,59)
        }
    }
})

# ============================================================
# DOBLE CLICK EN ARCHIVO
# ============================================================

$listFiles.Add_DoubleClick({

    if (
        -not $script:Converting -and
        $listFiles.SelectedIndex -ge 0
    ) {

        Refresh-CurrentFile
    }
})

# ============================================================
# ESC = CANCELAR
# ============================================================

$form.Add_KeyDown({

    if (
        $_.KeyCode -eq
        [System.Windows.Forms.Keys]::Escape
    ) {

        if ($script:Converting) {

            $script:CancelRequested =
                $true
        }
    }
})

$form.KeyPreview = $true

# ============================================================
# CERRAR VENTANA
# ============================================================

$form.Add_FormClosing({

    if ($script:Converting) {

        $result =
            [System.Windows.Forms.MessageBox]::Show(
                'Hay una conversión en curso. ¿Quieres salir?',
                'MKV Converter',
                [System.Windows.Forms.MessageBoxButtons]::YesNo,
                [System.Windows.Forms.MessageBoxIcon]::Warning
            )

        if (
            $result -ne
            [System.Windows.Forms.DialogResult]::Yes
        ) {

            $_.Cancel =
                $true

            return
        }

        $script:CancelRequested =
            $true

        $script:AllowClose =
            $true

        return
    }

    # --------------------------------------------------------
    # Si NO está convirtiendo, cerrar normalmente.
    # --------------------------------------------------------

    $script:AllowClose =
        $true
})

# ============================================================
# ============================================================
# PROTEGER CIERRE
# ============================================================

$form.Add_FormClosed({
    try {
        if ($null -ne $script:ActiveFfmpegProcess -and -not $script:ActiveFfmpegProcess.HasExited) {
            $script:ActiveFfmpegProcess.Kill()
        }
    }
    catch {}
})
# ============================================================
# ADAPTACION RESPONSIVA REAL A LA PANTALLA Y A LA VENTANA
# El diseño usa 1536x1024 como referencia, pero escala X e Y de
# forma independiente. Así aprovecha también todo el ancho disponible
# en pantallas panorámicas y se reajusta cada vez que la ventana cambia.
# ============================================================
$form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::None

$script:BaseClientWidth = 1536
$script:BaseClientHeight = 1024
$script:LayoutSnapshot = New-Object 'System.Collections.Generic.List[object]'

function Save-LayoutSnapshot {
    param([System.Windows.Forms.Control]$root)
    $script:LayoutSnapshot.Clear()
    function Save-ControlTree {
        param([System.Windows.Forms.Control]$parent)
        foreach ($c in $parent.Controls) {
            $fontSize = 0.0
            $fontStyle = [System.Drawing.FontStyle]::Regular
            $fontName = 'Segoe UI'
            try {
                $fontSize = [double]$c.Font.Size
                $fontStyle = $c.Font.Style
                $fontName = $c.Font.FontFamily.Name
            } catch {}
            $script:LayoutSnapshot.Add([pscustomobject]@{
                Control = $c
                X = [int]$c.Left
                Y = [int]$c.Top
                W = [int]$c.Width
                H = [int]$c.Height
                FontName = $fontName
                FontSize = $fontSize
                FontStyle = $fontStyle
            }) | Out-Null
            if ($c.Controls.Count -gt 0) { Save-ControlTree $c }
        }
    }
    Save-ControlTree $root
}

function Apply-ResponsiveLayout {
    param(
        [int]$targetW = 0,
        [int]$targetH = 0
    )

    if ($script:LayoutSnapshot.Count -eq 0) { return }

    if ($targetW -le 0) { $targetW = $form.ClientSize.Width }
    if ($targetH -le 0) { $targetH = $form.ClientSize.Height }

    $targetW = [Math]::Max(800,$targetW)
    $targetH = [Math]::Max(600,$targetH)

    # Escala independiente X/Y: en una pantalla panorámica se aprovecha
    # todo el ancho en vez de dejar una franja vacía a la derecha.
    $scaleX = [double]$targetW / [double]$script:BaseClientWidth
    $scaleY = [double]$targetH / [double]$script:BaseClientHeight
    if ($scaleX -le 0) { $scaleX = 1.0 }
    if ($scaleY -le 0) { $scaleY = 1.0 }

    $form.SuspendLayout()
    try {
        foreach ($item in $script:LayoutSnapshot) {
            $c = $item.Control
            if ($null -eq $c -or $c.IsDisposed) { continue }

            $c.Location = New-Object System.Drawing.Point(
                [int][Math]::Round($item.X * $scaleX),
                [int][Math]::Round($item.Y * $scaleY)
            )
            $c.Size = New-Object System.Drawing.Size(
                [Math]::Max(1,[int][Math]::Round($item.W * $scaleX)),
                [Math]::Max(1,[int][Math]::Round($item.H * $scaleY))
            )

            if ($item.FontSize -gt 0) {
                try {
                    # La fuente usa la escala menor para conservar legibilidad
                    # aunque la pantalla tenga una relación de aspecto distinta.
                    $fontScale = [Math]::Min($scaleX,$scaleY)
                    $newSize = [Math]::Max(6.0,$item.FontSize * $fontScale)
                    $c.Font = New-Object System.Drawing.Font($item.FontName,$newSize,$item.FontStyle)
                } catch {}
            }
        }
    }
    finally {
        $form.ResumeLayout($true)
    }
}

Save-LayoutSnapshot $form

$script:ApplyingResponsiveLayout = $false
$form.Add_Resize({
    if ($script:ApplyingResponsiveLayout) { return }
    if ($form.WindowState -eq [System.Windows.Forms.FormWindowState]::Minimized) { return }

    $script:ApplyingResponsiveLayout = $true
    try {
        # Al cambiar manualmente el tamaño, el contenido se reajusta al
        # nuevo ClientSize exacto de la ventana.
        Apply-ResponsiveLayout $form.ClientSize.Width $form.ClientSize.Height
    }
    finally {
        $script:ApplyingResponsiveLayout = $false
    }
})

# MOSTRAR
# ============================================================

$form.Add_Shown({
    $work = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
    $form.StartPosition = 'Manual'
    $form.Location = New-Object System.Drawing.Point($work.X,$work.Y)
    $form.ClientSize = New-Object System.Drawing.Size($work.Width,$work.Height)
    Apply-ResponsiveLayout $work.Width $work.Height
    $form.Activate()
})

# Application.Run es el bucle de mensajes nativo de Windows.
# La ventana no se cierra ni el BAT continúa hasta que el usuario
# la cierre explícitamente.
Register-TargetSizeCommitOnClick $form

[System.Windows.Forms.Application]::Run($form)

$ tail -f comments.log

Comments [0]

// no comments yet