2026년 11월 14일 토요일

KODI에서 영화 이어보기가 안 되고 멈추는 원인? 60GB짜리 가짜 용량(0x00 더미) 0.1초 만에 판별하는 전용 프로그램 배포

팁 & 노하우

KODI에서 영화 이어보기가 안 되고 멈추는 이유? 60GB짜리 가짜 용량(0x00 더미) 0.1초 만에 판별하기

처음부터 재생하면 잘 나오는데, 중간 탐색(Seek)이나 이어보기만 하면 NAS 하드만 긁고 멈추는 미스터리 해결기

1. 답답했던 증상: "왜 이어보기만 하면 무한 로딩일까?"

NAS에 저장된 4K HDR 고화질 영화(약 62GB)를 KODI로 시청하다가 중간에 끄고, 다음 날 '이어보기(Resume)'를 눌렀을 때 이상한 현상을 겪었습니다.

  • 화면은 까맣게 멈추고 버퍼링 로딩 휠만 계속 돕니다.
  • NAS 하드디스크는 미친 듯이 드르륵거리며 파일 읽는 소리만 납니다.
  • 수분을 기다려도 재생되지 않거나 결국 재생 실패 오류가 뜹니다.
  • 그런데 '처음부터 재생'을 누르면 앞부분은 완벽하게 잘 나옵니다!

윈도우 탐색기에서 봐도 61.9GB로 용량이 꽉 차 있고, MediaInfo로 확인해 봐도 코덱과 영상 길이가 정상적으로 인식되어 원인을 찾기 매우 어려웠습니다.

2. 원인 분석: 껍데기만 60GB, 속은 84%가 빈 데이터(0x00)

파일의 바이너리(Hex Data)를 직접 뜯어 이분 탐색으로 정밀 분석한 결과, 충격적인 사실이 밝혀졌습니다.

분석 항목 상세 내용 판별 결과
표기 파일 용량 66,532,099,453 바이트 약 61.96 GB
실제 유효 데이터 10,475,143,168 바이트 약 9.76 GB (15.7%)
빈 데이터 (0x00 더미) 56,056,956,285 바이트 약 52.2 GB (84.3% 허깨비) 🚨
💡 왜 이런 파일이 생겼을까요?
토렌트 클라이언트(Transmission, qBittorrent 등)는 다운로드를 시작할 때 디스크 단편화를 방지하기 위해 전체 파일 크기만큼 디스크 공간을 미리 0으로 할당(Pre-allocation)합니다.
이 영화는 다운로드가 약 15%쯤 되다가 시더(배포자)가 끊겨 멈췄는데, 파일 시스템상에는 62GB짜리 완전한 파일처럼 껍데기만 남아 있었던 것입니다.

왜 이어보기만 안 되었을까요?
MKV 파일은 영상 중간 위치로 점프할 때 필요한 인덱스 테이블(Cues)이 보통 파일 맨 뒷부분에 위치합니다. 하지만 그 위치가 순수 0x00으로 비어 있으니, KODI가 인덱스를 찾으려고 수십 GB의 빈 데이터를 네트워크 너머로 계속 훑느라 무한 로딩에 빠졌던 것입니다.

3. 0.1초 만에 가짜 용량을 잡아내는 원리

MediaInfo는 헤더만 읽어서 속고, 일반 영상 검사기는 60GB를 다 읽느라 몇 시간이 걸립니다. 이를 해결하기 위해 초고속 이분 탐색 알고리즘을 적용했습니다.

  1. 끝단 64KB 고속 체크 (1밀리초): 파일 맨 끝부분을 살짝 찍어보고, 0x00으로만 가득 차 있다면 즉시 '미완료 의심 파일'로 판단.
  2. 이분 탐색 (50밀리초): 파일 전체를 읽지 않고 위치만 반씩 건너뛰며(Seek) 10~15번만 확인하여, 실제 데이터가 몇 GB까지 들어있는지 정확한 완성도(%)를 0.1초 만에 계산.

4. 무료 배포: 윈도우용 초고속 판별기 (무설치 GUI)

아무것도 추가 설치할 필요 없이 윈도우 기본 기능(PowerShell + WPF)만으로 동작하는 단일 실행 배치 파일(.bat)입니다. 마우스로 파일이나 폴더를 끌어다 놓기만 하면 됩니다.

🛠️ 30초 만에 프로그램 만드는 법
  1. 바탕화면 빈 곳 우클릭 → [새로 만들기] → [텍스트 문서] 생성
  2. 파일 이름을 VideoZeroChecker.bat 으로 변경 (확장자가 .txt가 아닌 .bat이어야 함)
  3. 메모장으로 열어서 아래의 전체 코드를 붙여넣고 저장
VideoZeroChecker.bat
<# :
@echo off
setlocal
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0'))"
exit /b
#>

Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms

[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="동영상 가짜 용량(0x00 더미) 판별기 v1.0" Height="580" Width="920"
        WindowStartupLocation="CenterScreen" Background="#F8F9FA" AllowDrop="True">
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Padding" Value="12,6"/>
            <Setter Property="FontSize" Value="12"/>
            <Setter Property="FontWeight" Value="SemiBold"/>
            <Setter Property="Cursor" Value="Hand"/>
            <Setter Property="Margin" Value="4,0"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="Foreground" Value="White"/>
        </Style>
    </Window.Resources>
    
    <Grid Margin="16">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <Border Grid.Row="0" Background="#1E293B" CornerRadius="8" Padding="16,12" Margin="0,0,0,12">
            <StackPanel>
                <TextBlock Text="🎬 동영상 가짜 용량(토렌트 미완료 패딩) 초고속 판별기" FontSize="18" FontWeight="Bold" Foreground="White"/>
                <TextBlock Text="KODI 등에서 재생이 멈추거나 이어보기가 안 되는 0x00 더미 파일들을 1초 만에 감지합니다." FontSize="12" Foreground="#94A3B8" Margin="0,4,0,0"/>
            </StackPanel>
        </Border>

        <Grid Grid.Row="1" Margin="0,0,0,10">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <StackPanel Grid.Column="0" Orientation="Horizontal">
                <Button Name="BtnAddFiles" Content="📄 파일 추가" Background="#2563EB"/>
                <Button Name="BtnAddFolder" Content="📁 폴더 추가" Background="#475569"/>
                <Button Name="BtnClear" Content="🗑️ 목록 비우기" Background="#64748B"/>
            </StackPanel>
            <StackPanel Grid.Column="2" Orientation="Horizontal">
                <Button Name="BtnStartCheck" Content="▶️ 검사 시작" Background="#16A34A" MinWidth="110"/>
            </StackPanel>
        </Grid>

        <Border Grid.Row="2" BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="6" Background="White">
            <ListView Name="ListResults" BorderThickness="0" Background="Transparent" AlternationCount="2">
                <ListView.ItemContainerStyle>
                    <Style TargetType="ListViewItem">
                        <Setter Property="Padding" Value="4,6"/>
                        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        <Style.Triggers>
                            <Trigger Property="ItemsControl.AlternationIndex" Value="1">
                                <Setter Property="Background" Value="#F8FAFC"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </ListView.ItemContainerStyle>
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="파일명" Width="300" DisplayMemberBinding="{Binding FileName}"/>
                        <GridViewColumn Header="판별 상태" Width="140">
                            <GridViewColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Status}" FontWeight="Bold" Foreground="{Binding StatusColor}"/>
                                </DataTemplate>
                            </GridViewColumn.CellTemplate>
                        </GridViewColumn>
                        <GridViewColumn Header="표기 용량" Width="90" DisplayMemberBinding="{Binding TotalSize}"/>
                        <GridViewColumn Header="실제 데이터" Width="90" DisplayMemberBinding="{Binding ActualSize}"/>
                        <GridViewColumn Header="완성도 (%)" Width="85" DisplayMemberBinding="{Binding ValidPercent}"/>
                        <GridViewColumn Header="파일 경로" Width="280" DisplayMemberBinding="{Binding FilePath}"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </Border>

        <Grid Grid.Row="3" Margin="0,10,0,0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="200"/>
            </Grid.ColumnDefinitions>
            <TextBlock Name="TxtStatus" Text="파일이나 폴더를 이곳으로 드래그 앤 드롭하세요." VerticalAlignment="Center" FontSize="12" Foreground="#475569"/>
            <ProgressBar Name="ProgBar" Grid.Column="1" Height="14" Minimum="0" Maximum="100" Value="0" Visibility="Collapsed"/>
        </Grid>
    </Grid>
</Window>
"@

$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [System.Windows.Markup.XamlReader]::Load($reader)

$btnFiles = $window.FindName("BtnAddFiles")
$btnFolder = $window.FindName("BtnAddFolder")
$btnClear = $window.FindName("BtnClear")
$btnStart = $window.FindName("BtnStartCheck")
$listView = $window.FindName("ListResults")
$txtStatus = $window.FindName("TxtStatus")
$progBar = $window.FindName("ProgBar")

$videoExtensions = @(".mkv", ".mp4", ".ts", ".m2ts", ".avi", ".mov", ".wmv", ".iso")
$fileList = New-Object System.Collections.ObjectModel.ObservableCollection[Object]
$listView.ItemsSource = $fileList

function Add-FileToList($path) {
    if (-not (Test-Path -LiteralPath $path)) { return }
    $ext = [System.IO.Path]::GetExtension($path).ToLower()
    if ($videoExtensions -contains $ext) {
        foreach ($item in $fileList) {
            if ($item.FilePath -eq $path) { return }
        }
        $info = Get-Item -LiteralPath $path
        $fileList.Add([PSCustomObject]@{
            FileName = $info.Name
            Status = "대기 중"
            StatusColor = "#64748B"
            TotalSize = "$([Math]::Round($info.Length / 1GB, 2)) GB"
            ActualSize = "-"
            ValidPercent = "-"
            FilePath = $info.FullName
            RawLength = $info.Length
        })
    }
}

$window.Add_Drop({
    param($s, $e)
    if ($e.Data.GetDataPresent([System.Windows.DataFormats]::FileDrop)) {
        $dropped = $e.Data.GetData([System.Windows.DataFormats]::FileDrop)
        foreach ($item in $dropped) {
            if ((Get-Item -LiteralPath $item) -is [System.IO.DirectoryInfo]) {
                Get-ChildItem -LiteralPath $item -Recurse -File | ForEach-Object { Add-FileToList $_.FullName }
            } else {
                Add-FileToList $item
            }
        }
        $txtStatus.Text = "총 $($fileList.Count)개 파일이 등록되었습니다. '검사 시작'을 누르세요."
    }
})

$btnFiles.Add_Click({
    $dlg = New-Object Microsoft.Win32.OpenFileDialog
    $dlg.Multiselect = $true
    $dlg.Filter = "동영상 파일|*.mkv;*.mp4;*.ts;*.m2ts;*.avi;*.mov;*.wmv;*.iso|모든 파일|*.*"
    if ($dlg.ShowDialog() -eq $true) {
        foreach ($file in $dlg.FileNames) { Add-FileToList $file }
        $txtStatus.Text = "총 $($fileList.Count)개 파일이 대기 중입니다."
    }
})

$btnFolder.Add_Click({
    $fbd = New-Object System.Windows.Forms.FolderBrowserDialog
    $fbd.Description = "검사할 동영상 폴더를 선택하세요."
    if ($fbd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
        Get-ChildItem -LiteralPath $fbd.SelectedPath -Recurse -File | ForEach-Object { Add-FileToList $_.FullName }
        $txtStatus.Text = "총 $($fileList.Count)개 파일이 대기 중입니다."
    }
})

$btnClear.Add_Click({
    $fileList.Clear()
    $txtStatus.Text = "목록을 비웠습니다."
    $progBar.Value = 0
    $progBar.Visibility = "Collapsed"
})

$btnStart.Add_Click({
    if ($fileList.Count -eq 0) {
        [System.Windows.MessageBox]::Show("검사할 파일이나 폴더를 먼저 추가해 주세요.", "알림", "OK", "Information")
        return
    }

    $btnStart.IsEnabled = $false
    $progBar.Visibility = "Visible"
    $progBar.Maximum = $fileList.Count
    $progBar.Value = 0

    $fakeCount = 0

    for ($idx = 0; $idx -lt $fileList.Count; $idx++) {
        $item = $fileList[$idx]
        $filePath = $item.FilePath
        $fileSize = $item.RawLength
        $txtStatus.Text = "검사 중 ($($idx + 1)/$($fileList.Count)): $($item.FileName)"
        $progBar.Value = $idx + 1

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

        try {
            $stream = [System.IO.File]::Open($filePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)

            $sample = [Math]::Min($fileSize, 65536)
            $stream.Seek($fileSize - $sample, [System.IO.SeekOrigin]::Begin) | Out-Null
            $buf = New-Object byte[] $sample
            $read = $stream.Read($buf, 0, $sample)

            $isTailZero = $true
            for ($k = 0; $k -lt $read; $k++) {
                if ($buf[$k] -ne 0) { $isTailZero = $false; break }
            }

            if (-not $isTailZero) {
                $stream.Close()
                $fileList[$idx] = [PSCustomObject]@{
                    FileName = $item.FileName
                    Status = "정상"
                    StatusColor = "#16A34A"
                    TotalSize = $item.TotalSize
                    ActualSize = $item.TotalSize
                    ValidPercent = "100 %"
                    FilePath = $item.FilePath
                    RawLength = $item.RawLength
                }
            } else {
                $lo = [long]0
                $hi = $fileSize
                $chk = 4096
                $cbuf = New-Object byte[] $chk

                while (($hi - $lo) -gt $chk) {
                    $mid = $lo + (($hi - $lo) / 2)
                    $mid = [long]($mid - ($mid % $chk))
                    $stream.Seek($mid, [System.IO.SeekOrigin]::Begin) | Out-Null
                    $r = $stream.Read($cbuf, 0, $chk)

                    $isZ = $true
                    for ($z = 0; $z -lt $r; $z++) {
                        if ($cbuf[$z] -ne 0) { $isZ = $false; break }
                    }

                    if ($isZ) { $hi = $mid } else { $lo = $mid + $chk }
                }
                $stream.Close()

                $actualBytes = $lo
                $validPct = [Math]::Round(($actualBytes / $fileSize) * 100, 2)

                if ((100 - $validPct) -ge 3.0) {
                    $fakeCount++
                    $fileList[$idx] = [PSCustomObject]@{
                        FileName = $item.FileName
                        Status = "🚨 가짜 용량 (미완료)"
                        StatusColor = "#DC2626"
                        TotalSize = $item.TotalSize
                        ActualSize = "$([Math]::Round($actualBytes / 1GB, 2)) GB"
                        ValidPercent = "$validPct %"
                        FilePath = $item.FilePath
                        RawLength = $item.RawLength
                    }
                } else {
                    $fileList[$idx] = [PSCustomObject]@{
                        FileName = $item.FileName
                        Status = "정상"
                        StatusColor = "#16A34A"
                        TotalSize = $item.TotalSize
                        ActualSize = $item.TotalSize
                        ValidPercent = "100 %"
                        FilePath = $item.FilePath
                        RawLength = $item.RawLength
                    }
                }
            }
        }
        catch {
            $fileList[$idx] = [PSCustomObject]@{
                FileName = $item.FileName
                Status = "❌ 접근 실패"
                StatusColor = "#EA580C"
                TotalSize = $item.TotalSize
                ActualSize = "-"
                ValidPercent = "-"
                FilePath = $item.FilePath
                RawLength = $item.RawLength
            }
        }
    }

    $btnStart.IsEnabled = $true
    if ($fakeCount -gt 0) {
        $txtStatus.Text = "검사 완료: 총 $($fileList.Count)개 중 $fakeCount개의 미완료/손상 파일이 발견되었습니다!"
        [System.Windows.MessageBox]::Show("검사가 완료되었습니다.`n발견된 가짜/미완료 파일: ${fakeCount}개", "검사 완료", "OK", "Warning")
    } else {
        $txtStatus.Text = "검사 완료: 모든 파일이 100% 정상입니다."
        [System.Windows.MessageBox]::Show("모든 파일이 정상입니다!", "검사 완료", "OK", "Information")
    }
})

$window.ShowDialog() | Out-Null

마치며

KODI나 nPlayer, Plex 등에서 특정 영상만 이어보기가 안 되고 무한 버퍼링에 걸린다면, 플레이어나 공유기 설정을 의심하기 전에 이 도구로 파일의 실제 완성도를 꼭 확인해 보세요!



댓글 없음:

댓글 쓰기