Skip to content

Instantly share code, notes, and snippets.

@miiiladiii244
Last active September 9, 2025 19:35
Show Gist options
  • Select an option

  • Save miiiladiii244/e13f6bf01e84e0d7353fca7124fbd95c to your computer and use it in GitHub Desktop.

Select an option

Save miiiladiii244/e13f6bf01e84e0d7353fca7124fbd95c to your computer and use it in GitHub Desktop.
Check for existence of a list of files under a directory
<#
.SYNOPSIS
Fast existence check for files listed in a text/playlist against a folder tree.
.DESCRIPTION
- Indexes all files under -RootPath (default: current folder) once.
- For each line in -ListPath, checks existence by:
1) Absolute path match
2) Relative path match (case-insensitive) within the root tree
3) Optional leaf-name fallback anywhere in the tree (-AllowLeafFallback)
Skips comments (#...) and URLs. Writes missing lines to an output file.
.PARAMETER ListPath
Path to the playlist/file list (e.g., .pla, .m3u, .txt).
.PARAMETER RootPath
Folder to search under (defaults to current directory).
.PARAMETER OutputPath
File to write missing lines to (defaults to "missing-files.txt" under RootPath).
.PARAMETER AllowLeafFallback
If set, a line is considered found if a file with the same leaf name exists anywhere under RootPath.
.EXAMPLE
PS> Check-FilesIndexed.ps1 -ListPath "C:\Lists\New Playlist.pla"
.EXAMPLE
PS> Check-FilesIndexed.ps1 -ListPath ".\mylist.txt" -RootPath "D:\Media" -AllowLeafFallback
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]$ListPath,
[Parameter(Mandatory = $false)]
[string]$RootPath = (Get-Location).ProviderPath,
[Parameter(Mandatory = $false)]
[string]$OutputPath = $(Join-Path -Path ((Get-Item -LiteralPath $RootPath).FullName) -ChildPath "missing-files.txt"),
[switch]$AllowLeafFallback
)
# ---- Guards ----
if (-not (Test-Path -LiteralPath $ListPath -PathType Leaf)) {
Write-Error "List file not found: $ListPath"
exit 1
}
if (-not (Test-Path -LiteralPath $RootPath -PathType Container)) {
Write-Error "Root path folder not found: $RootPath"
exit 1
}
# ---- Helpers ----
function Normalize-InputPath([string]$s) {
if (-not $s) { return $null }
$t = $s.Trim()
if ($t -eq "") { return $null }
if ($t.StartsWith("#")) { return $null } # comment
if ($t -match "^[a-zA-Z]+://") { return $null } # URL
# strip outer quotes
if (($t.StartsWith('"') -and $t.EndsWith('"')) -or ($t.StartsWith("'") -and $t.EndsWith("'"))) {
$t = $t.Substring(1, $t.Length - 2)
}
# expand env vars like %USERPROFILE%
$t = [Environment]::ExpandEnvironmentVariables($t)
# unify separators and trim leading .\ or ./ and leading slashes
$t = $t -replace '/', '\'
if ($t -like ".\*") { $t = $t.Substring(2) }
$t = $t.TrimStart('\')
return $t
}
# Compute relative path of a full path to the RootPath (case-insensitive)
function To-Relative([string]$full, [string]$root) {
$rootNorm = $root.TrimEnd('\')
if ($full.StartsWith($rootNorm, [System.StringComparison]::OrdinalIgnoreCase)) {
$rel = $full.Substring($rootNorm.Length).TrimStart('\')
return $rel
}
return $null
}
# ---- Index the tree under RootPath ----
Write-Verbose "Indexing files under: $RootPath"
$allFiles = Get-ChildItem -LiteralPath $RootPath -Recurse -File -ErrorAction SilentlyContinue
# HashSet of full paths (lowercased) and relative paths (lowercased)
$fullSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
$relSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
# Dictionary of leaf name -> count (case-insensitive)
$leafDict = New-Object 'System.Collections.Generic.Dictionary[string,int]' ([System.StringComparer]::OrdinalIgnoreCase)
$rootFull = (Get-Item -LiteralPath $RootPath).FullName
foreach ($f in $allFiles) {
$fullSet.Add($f.FullName) | Out-Null
$rel = To-Relative -full $f.FullName -root $rootFull
if ($rel) { $relSet.Add($rel) | Out-Null }
$leaf = $f.Name
if ($leafDict.ContainsKey($leaf)) { $leafDict[$leaf]++ } else { $leafDict[$leaf] = 1 }
}
# ---- Process the list ----
$missing = New-Object System.Collections.Generic.List[string]
foreach ($raw in Get-Content -LiteralPath $ListPath -Encoding UTF8) {
$p = Normalize-InputPath $raw
if (-not $p) { continue }
$exists = $false
if ([System.IO.Path]::IsPathRooted($p)) {
# Absolute path: direct hit or relative-to-root appearance
if ($fullSet.Contains($p)) {
$exists = $true
} else {
$relCandidate = To-Relative -full $p -root $rootFull
if ($relCandidate -and $relSet.Contains($relCandidate)) {
$exists = $true
}
}
} else {
# Relative path: exact relative match or combined path match
if ($relSet.Contains($p)) {
$exists = $true
} else {
$candidate = Join-Path -Path $rootFull -ChildPath $p
if ($fullSet.Contains($candidate)) {
$exists = $true
}
}
}
if (-not $exists -and $AllowLeafFallback) {
try {
$leaf = [System.IO.Path]::GetFileName($p)
if ($leaf -and $leafDict.ContainsKey($leaf) -and $leafDict[$leaf] -ge 1) {
$exists = $true
}
} catch { }
}
if (-not $exists) {
$missing.Add($raw) | Out-Null # keep original line for clarity
}
}
# ---- Output ----
if ($missing.Count -eq 0) {
Write-Host "All listed files were found under '$RootPath'." -ForegroundColor Green
} else {
$missing | Set-Content -LiteralPath $OutputPath -Encoding UTF8
Write-Host "Missing files ($($missing.Count)):" -ForegroundColor Yellow
$missing | ForEach-Object { Write-Host " $_" }
Write-Host "`nSaved to: $OutputPath" -ForegroundColor Yellow
}
@miiiladiii244
Copy link
Author

Search under the current folder

Check-FilesIndexed.ps1 -ListPath "C:\Users\miiiladiii\Music\New Playlist.pla"

Search under a specific root

Check-FilesIndexed.ps1 -ListPath ".\list.txt" -RootPath "D:\Media"

Allow loose leaf-name fallback (treat 'song.mp3' as found if any 'song.mp3' exists anywhere under the root)

Check-FilesIndexed.ps1 -ListPath ".\list.txt" -AllowLeafFallback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment