Created
January 17, 2026 19:57
-
-
Save BenMcLean/b762921479f16fbbf40aa5452825246b to your computer and use it in GitHub Desktop.
Organize ROMs into Alphabetical Range Folders
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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