Skip to content

Instantly share code, notes, and snippets.

@BenMcLean
Created January 17, 2026 19:57
Show Gist options
  • Select an option

  • Save BenMcLean/b762921479f16fbbf40aa5452825246b to your computer and use it in GitHub Desktop.

Select an option

Save BenMcLean/b762921479f16fbbf40aa5452825246b to your computer and use it in GitHub Desktop.
Organize ROMs into Alphabetical Range Folders
# Organize ROMs into Alphabetical Range Folders
# This script moves ROM files into folders based on alphabetical ranges and truncates long filenames
param(
[string]$Path = ".",
[int]$MaxFilesPerFolder = 256,
[int]$MaxFilenameLength = 99,
[switch]$WhatIf
)
Write-Host "ROM Organization Script" -ForegroundColor Cyan
Write-Host "======================" -ForegroundColor Cyan
Write-Host "Source Path: $Path" -ForegroundColor White
Write-Host "Max files per folder: $MaxFilesPerFolder" -ForegroundColor White
Write-Host "Max filename length: $MaxFilenameLength characters (adjusts for extension)" -ForegroundColor White
if ($WhatIf) {
Write-Host "MODE: DRY RUN (no files will be moved)" -ForegroundColor Yellow
}
Write-Host ""
# Get all files in the directory (excluding directories)
$files = Get-ChildItem -Path $Path -File -ErrorAction SilentlyContinue
$totalFiles = $files.Count
if ($totalFiles -eq 0) {
Write-Host "No files found" -ForegroundColor Yellow
exit
}
Write-Host "Total files found: $totalFiles" -ForegroundColor Green
Write-Host ""
# Count files by first letter (case insensitive) and non-alphabetical
$letterCounts = @{}
65..90 | ForEach-Object {
$letter = [char]$_
$letterCounts[$letter.ToString()] = 0
}
$letterCounts['0-9'] = 0
foreach ($file in $files) {
if ($file.Name.Length -eq 0) { continue }
$firstChar = $file.Name.Substring(0,1).ToUpper()
if ($firstChar -match '[A-Z]') {
$letterCounts[$firstChar]++
} else {
# Everything else (numbers, special chars, etc.) goes to 0-9
$letterCounts['0-9']++
}
}
# Function to determine optimal folder structure
function Get-OptimalFolderStructure {
param([int]$MaxFiles)
for ($numFolders = 3; $numFolders -le 26; $numFolders++) {
$lettersPerFolder = [Math]::Ceiling(26 / $numFolders)
$maxInAnyFolder = 0
$ranges = @()
for ($i = 0; $i -lt $numFolders; $i++) {
$startIdx = $i * $lettersPerFolder
$endIdx = [Math]::Min(($i + 1) * $lettersPerFolder - 1, 25)
if ($startIdx -gt 25) { break }
$startLetter = [char]([int]65 + [int]$startIdx)
$endLetter = [char]([int]65 + [int]$endIdx)
$count = 0
for ($j = $startIdx; $j -le $endIdx; $j++) {
$letter = [char]([int]65 + [int]$j)
$letterStr = $letter.ToString()
if ($letterCounts.ContainsKey($letterStr)) {
$count += $letterCounts[$letterStr]
}
}
$ranges += [PSCustomObject]@{
StartLetter = $startLetter.ToString()
EndLetter = $endLetter.ToString()
FolderName = "$startLetter-$endLetter"
Count = $count
}
$maxInAnyFolder = [Math]::Max($maxInAnyFolder, $count)
}
if ($maxInAnyFolder -lt $MaxFiles) {
return $ranges
}
}
# If we can't find a good solution, just return single-letter folders
Write-Host "Warning: Could not find optimal range, using single letters" -ForegroundColor Yellow
return $null
}
# Get the folder structure
$folderStructure = Get-OptimalFolderStructure -MaxFiles $MaxFilesPerFolder
if ($null -eq $folderStructure) {
Write-Host "Error: Could not determine folder structure" -ForegroundColor Red
exit
}
Write-Host "Folder Structure:" -ForegroundColor Cyan
foreach ($range in $folderStructure) {
Write-Host (" {0,-6} : {1,4} files" -f $range.FolderName, $range.Count)
}
# Add 0-9 folder if there are non-alphabetical files
if ($letterCounts['0-9'] -gt 0) {
Write-Host (" {0,-6} : {1,4} files" -f "0-9", $letterCounts['0-9'])
}
Write-Host ""
# Create a lookup table for quick folder assignment
$letterToFolder = @{}
foreach ($range in $folderStructure) {
$startCode = [int][char]$range.StartLetter
$endCode = [int][char]$range.EndLetter
for ($i = $startCode; $i -le $endCode; $i++) {
$letter = [char]$i
$letterToFolder[$letter.ToString()] = $range.FolderName
}
}
# Create folders if not in WhatIf mode
if (-not $WhatIf) {
Write-Host "Creating folders..." -ForegroundColor Cyan
foreach ($range in $folderStructure) {
$folderPath = Join-Path -Path $Path -ChildPath $range.FolderName
if (-not (Test-Path -Path $folderPath)) {
New-Item -Path $folderPath -ItemType Directory -Force | Out-Null
Write-Host " Created: $($range.FolderName)" -ForegroundColor Green
}
}
# Create 0-9 folder if needed
if ($letterCounts['0-9'] -gt 0) {
$folderPath = Join-Path -Path $Path -ChildPath "0-9"
if (-not (Test-Path -Path $folderPath)) {
New-Item -Path $folderPath -ItemType Directory -Force | Out-Null
Write-Host " Created: 0-9" -ForegroundColor Green
}
}
Write-Host ""
}
# Move files
Write-Host "Processing files..." -ForegroundColor Cyan
$moved = 0
$truncated = 0
$errors = 0
$skipped = 0
foreach ($file in $files) {
try {
if ($file.Name.Length -eq 0) {
$skipped++
continue
}
$firstChar = $file.Name.Substring(0,1).ToUpper()
# Determine target folder based on first character
$targetFolder = $null
if ($firstChar -match '[A-Z]') {
if (-not $letterToFolder.ContainsKey($firstChar)) {
Write-Host " WARNING: No folder mapping for '$firstChar' in file: $($file.Name)" -ForegroundColor Yellow
$skipped++
continue
}
$targetFolder = $letterToFolder[$firstChar]
} else {
# Everything else goes to 0-9
$targetFolder = "0-9"
}
$targetFolderPath = Join-Path -Path $Path -ChildPath $targetFolder
# Check if filename needs truncation and handle duplicates smartly
$extension = [System.IO.Path]::GetExtension($file.Name)
$nameWithoutExt = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
# Calculate max length: total limit minus extension length
$maxNameLength = $MaxFilenameLength - $extension.Length
# Truncate if needed
if ($nameWithoutExt.Length -gt $maxNameLength) {
$nameWithoutExt = $nameWithoutExt.Substring(0, $maxNameLength)
$truncated++
if ($WhatIf) {
Write-Host " [TRUNCATE] $($file.Name)" -ForegroundColor Yellow
}
}
# Now build the target filename and handle duplicates
$newFileName = "$nameWithoutExt$extension"
$targetPath = Join-Path -Path $targetFolderPath -ChildPath $newFileName
# Handle duplicate filenames
if (Test-Path -LiteralPath $targetPath) {
$counter = 1
do {
# Add counter, then check if we need to re-truncate
$numberedName = "${nameWithoutExt}_$counter"
# If adding the counter made it too long, truncate again
if ($numberedName.Length -gt $maxNameLength) {
# Make room for the counter by truncating more
$counterLength = "_$counter".Length
$numberedName = $nameWithoutExt.Substring(0, $maxNameLength - $counterLength) + "_$counter"
}
$newFileName = "$numberedName$extension"
$targetPath = Join-Path -Path $targetFolderPath -ChildPath $newFileName
$counter++
# Safety check
if ($counter -gt 1000) {
Write-Host " ERROR: Too many duplicate variations for $($file.Name)" -ForegroundColor Red
$errors++
$newFileName = $null
break
}
} while (Test-Path -LiteralPath $targetPath)
if ($WhatIf -and $newFileName) {
Write-Host " [DUPLICATE] -> $newFileName" -ForegroundColor Yellow
}
}
# Skip if we hit the duplicate limit
if ($null -eq $newFileName) {
$skipped++
continue
}
if ($WhatIf) {
Write-Host " [MOVE] $($file.Name) -> $targetFolder\$newFileName" -ForegroundColor Cyan
} else {
Move-Item -LiteralPath $file.FullName -Destination $targetPath -Force -ErrorAction Stop
$moved++
if ($moved % 50 -eq 0) {
Write-Host " Moved $moved files..." -ForegroundColor Gray
}
}
} catch {
$errors++
Write-Host " ERROR: $($file.Name) - $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "Summary:" -ForegroundColor Cyan
Write-Host "========" -ForegroundColor Cyan
if ($WhatIf) {
Write-Host "DRY RUN - No files were actually moved" -ForegroundColor Yellow
Write-Host "Files that would be moved: $($totalFiles - $skipped)" -ForegroundColor White
Write-Host "Files that would be truncated: $truncated" -ForegroundColor White
Write-Host "Files skipped: $skipped" -ForegroundColor White
} else {
Write-Host "Files moved: $moved" -ForegroundColor Green
Write-Host "Files truncated: $truncated" -ForegroundColor Yellow
Write-Host "Files skipped: $skipped" -ForegroundColor $(if ($skipped -gt 0) { "Yellow" } else { "Green" })
Write-Host "Errors: $errors" -ForegroundColor $(if ($errors -gt 0) { "Red" } else { "Green" })
}
Write-Host ""
Write-Host "Done!" -ForegroundColor Green
# Undo ROM Organization
# This script moves all files from subfolders back to the root directory and deletes the subfolders
param(
[string]$Path = ".",
[switch]$WhatIf
)
Write-Host "ROM Organization Undo Script" -ForegroundColor Cyan
Write-Host "============================" -ForegroundColor Cyan
Write-Host "Source Path: $Path" -ForegroundColor White
if ($WhatIf) {
Write-Host "MODE: DRY RUN (no files will be moved)" -ForegroundColor Yellow
}
Write-Host ""
# Get all subdirectories
$subfolders = Get-ChildItem -Path $Path -Directory
if ($subfolders.Count -eq 0) {
Write-Host "No subfolders found - nothing to undo" -ForegroundColor Yellow
exit
}
Write-Host "Found $($subfolders.Count) subfolder(s):" -ForegroundColor Green
foreach ($folder in $subfolders) {
$fileCount = (Get-ChildItem -Path $folder.FullName -File).Count
Write-Host " $($folder.Name): $fileCount files" -ForegroundColor White
}
Write-Host ""
# Move files back to root
Write-Host "Moving files back to root..." -ForegroundColor Cyan
$moved = 0
$errors = 0
foreach ($folder in $subfolders) {
$files = Get-ChildItem -Path $folder.FullName -File
foreach ($file in $files) {
try {
$targetPath = Join-Path -Path $Path -ChildPath $file.Name
# Handle duplicate filenames
if (Test-Path -Path $targetPath) {
$counter = 1
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
$extension = [System.IO.Path]::GetExtension($file.Name)
while (Test-Path -Path $targetPath) {
$newName = "${baseName}_$counter$extension"
$targetPath = Join-Path -Path $Path -ChildPath $newName
$counter++
}
if ($WhatIf) {
Write-Host " [RENAME] $($file.Name) -> $newName (duplicate)" -ForegroundColor Yellow
}
}
if ($WhatIf) {
Write-Host " [MOVE] $($folder.Name)\$($file.Name) -> root" -ForegroundColor Cyan
} else {
Move-Item -Path $file.FullName -Destination $targetPath -Force
$moved++
if ($moved % 50 -eq 0) {
Write-Host " Moved $moved files..." -ForegroundColor Gray
}
}
} catch {
$errors++
Write-Host " ERROR: $($file.Name) - $($_.Exception.Message)" -ForegroundColor Red
}
}
}
Write-Host ""
Write-Host "Deleting subfolders..." -ForegroundColor Cyan
foreach ($folder in $subfolders) {
try {
if ($WhatIf) {
Write-Host " [DELETE] $($folder.Name)" -ForegroundColor Yellow
} else {
Remove-Item -Path $folder.FullName -Recurse -Force
Write-Host " Deleted: $($folder.Name)" -ForegroundColor Green
}
} catch {
Write-Host " ERROR: Could not delete $($folder.Name) - $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "Summary:" -ForegroundColor Cyan
Write-Host "========" -ForegroundColor Cyan
if ($WhatIf) {
Write-Host "DRY RUN - No changes were made" -ForegroundColor Yellow
} else {
Write-Host "Files moved back: $moved" -ForegroundColor Green
Write-Host "Errors: $errors" -ForegroundColor $(if ($errors -gt 0) { "Red" } else { "Green" })
}
Write-Host ""
Write-Host "Done!" -ForegroundColor Green
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment