Indentify your movies with missing funscripts or funscripts with missing movies

I got tired of manually looking at files in Explorer, and I don’t care to use a more significant player/library management tool to find a few problematic funscripts that are “missing” a movie or movies without a funscript. I wanted something simple that I could run per directory or recursively to help find the actual gaps or where I just need to make a rename.

Just save/rename the below as CheckFunscripts.ps1 and run in Powershell:

powershell -ExecutionPolicy Bypass -File .\CheckFunscripts.ps1

Or run recursively:

powershell -ExecutionPolicy Bypass -File .\CheckFunscripts.ps1 -Recurse

CheckFunscripts.txt (5.6 KB)

param(
    [Parameter(Position=0)]
    [string]$Root = (Get-Location).Path,

    [switch]$Recurse,

    # Add/remove extensions here (no dots)
    [string[]]$MovieExts = @('mp4','mkv','avi','mov','m4v','wmv','webm','mpg','mpeg','ts','m2ts','mts','flv','3gp','vob'),

    [string]$ReportPath
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Get-TrimmedAtLastDot {
    param([string]$Name)
    $idx = $Name.LastIndexOf('.')
    if ($idx -lt 0) { return $null }
    return $Name.Substring(0, $idx)
}

# Resolve root reliably (mapped or UNC)
try { $rootItem = Get-Item -LiteralPath $Root }
catch { throw "Root path not found or not accessible: $Root" }
$Root = $rootItem.FullName

if (-not $ReportPath) {
    $ReportPath = Join-Path $Root 'funscript_audit_report.txt'
}

$gciBase = @{
    LiteralPath = $Root
    File        = $true
}
if ($Recurse) { $gciBase.Recurse = $true }

# ---------- Enumerate movies (targeted) ----------
$movieFiles = New-Object System.Collections.Generic.List[System.IO.FileInfo]
foreach ($ext in $MovieExts) {
    $pattern = "*.$ext"
    foreach ($f in (Get-ChildItem @gciBase -Filter $pattern)) {
        $movieFiles.Add($f)
    }
}

# ---------- Enumerate funscripts (targeted) ----------
$funscriptFiles = @(Get-ChildItem @gciBase -Filter '*.funscript')

# ---------- Build movie sets ----------
$movieNameSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
$movieBaseToPaths = @{}

foreach ($m in $movieFiles) {
    $base = [System.IO.Path]::GetFileNameWithoutExtension($m.Name)  # dots preserved
    [void]$movieNameSet.Add($base)

    if (-not $movieBaseToPaths.ContainsKey($base)) {
        $movieBaseToPaths[$base] = New-Object System.Collections.Generic.List[string]
    }
    $movieBaseToPaths[$base].Add($m.FullName)
}

# ---------- Build funscript key set ----------
$funscriptKeySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
$funscriptInfo = New-Object System.Collections.Generic.List[object]

foreach ($f in $funscriptFiles) {
    $fullBase = [System.IO.Path]::GetFileNameWithoutExtension($f.Name)
    $trimBase = Get-TrimmedAtLastDot $fullBase

    # Add full base (supports dotted movie names)
    [void]$funscriptKeySet.Add($fullBase)

    # Add trimmed base (supports axis variants)
    if ($trimBase -and ($trimBase -ne $fullBase)) {
        [void]$funscriptKeySet.Add($trimBase)
    }

    $funscriptInfo.Add([pscustomobject]@{
        Path     = $f.FullName
        FullBase = $fullBase
        TrimBase = $trimBase
    })
}

# ---------- Movies missing funscripts ----------
$moviesMissingFunscriptPaths = New-Object System.Collections.Generic.List[string]
foreach ($kvp in $movieBaseToPaths.GetEnumerator()) {
    $movieBase = $kvp.Key
    if (-not $funscriptKeySet.Contains($movieBase)) {
        foreach ($p in $kvp.Value) { $moviesMissingFunscriptPaths.Add($p) }
    }
}

# ---------- Funscripts missing movies ----------
$funscriptsMissingMoviePaths = New-Object System.Collections.Generic.List[string]
foreach ($fi in $funscriptInfo) {
    $matched = $movieNameSet.Contains($fi.FullBase) -or ($fi.TrimBase -and $movieNameSet.Contains($fi.TrimBase))
    if (-not $matched) { $funscriptsMissingMoviePaths.Add($fi.Path) }
}

# ---------- Sorting (force string arrays) ----------
$moviesMissingSorted = @($moviesMissingFunscriptPaths.ToArray() | Sort-Object)
$funscriptsMissingSorted = @($funscriptsMissingMoviePaths.ToArray() | Sort-Object)

# ---------- Summary ----------
$mode = if ($Recurse) { 'Recursive' } else { 'Non-recursive' }
$now = Get-Date

$movieFileCount = $movieFiles.Count
$funscriptFileCount = $funscriptFiles.Count
$uniqueMovieNames = $movieNameSet.Count
$uniqueFunscriptKeys = $funscriptKeySet.Count

# ---------- Write report ----------
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add('============================================================')
$lines.Add('FUNSCRIPT / MOVIE AUDIT REPORT')
$lines.Add('============================================================')
$lines.Add("Generated: $now")
$lines.Add("Root:      $Root")
$lines.Add("Mode:      $mode")
$lines.Add("Movie exts: " + ($MovieExts -join ' '))
$lines.Add('')
$lines.Add("Movie files found:            $movieFileCount")
$lines.Add("Funscript files found:        $funscriptFileCount")
$lines.Add("Unique movie base names:      $uniqueMovieNames")
$lines.Add("Unique funscript match keys:  $uniqueFunscriptKeys")
$lines.Add('')
$lines.Add("Movies missing funscript:     $($moviesMissingSorted.Count)")
$lines.Add("Funscripts missing movie:     $($funscriptsMissingSorted.Count)")
$lines.Add('============================================================')
$lines.Add('')
$lines.Add('[1] MOVIES WITHOUT A CORRESPONDING FUNSCRIPT')
$lines.Add('------------------------------------------------------------')

if ($moviesMissingSorted.Count -eq 0) {
    $lines.Add('(none)')
} else {
    foreach ($p in $moviesMissingSorted) { $lines.Add([string]$p) }
}

$lines.Add('')
$lines.Add('')
$lines.Add('[2] FUNSCRIPTS WITHOUT A CORRESPONDING MOVIE')
$lines.Add('------------------------------------------------------------')

if ($funscriptsMissingSorted.Count -eq 0) {
    $lines.Add('(none)')
} else {
    foreach ($p in $funscriptsMissingSorted) { $lines.Add([string]$p) }
}

$lines.Add('')
$lines.Add('============================================================')
$lines.Add('End of report.')

$lines | Set-Content -LiteralPath $ReportPath -Encoding UTF8

Write-Host "Report written to:"
Write-Host $ReportPath

1 Like