<# .SYNOPSIS Sorts games into tier folders (Must-Play, Great, Good, Average, Niche) from a CSV of titles and review scores. .DESCRIPTION Reads a CSV with Title and Score columns (a 0-100 score from Metacritic, a review site or your own opinion). For each game file in the folder it finds the matching row, working out the tier from the score: 90 and up: 1 Must-Play, 80-89: 2 Great, 70-79: 3 Good, 60-69: 4 Average, below 60: 5 Niche and moves the file into that tier's subfolder, creating the folders it needs. A row matches when its Title appears anywhere in the file's title, ignoring case and punctuation, and the longest match wins. That lets a short row like "Mario Party" act as a fallback for a whole series, while a full title still beats it. Files with no match stay where they are (Status Unrated), or go to -UnratedFolder if you name one. Existing files are never overwritten. Every file gets an object back. Supports -WhatIf. .PARAMETER Path The folder of games. Only files directly in it are sorted, so running it twice doesn't re-sort the tier folders. .PARAMETER ScoreCsv CSV with Title and Score columns. .PARAMETER Extension File extensions to sort. Default: every file. .PARAMETER Threshold The four cut-off scores, highest first. Default: 90, 80, 70, 60. .PARAMETER TierName The five folder names, best first. .PARAMETER UnratedFolder Folder name for games with no score. Default: leave them where they are. .EXAMPLE .\Move-RomToQualityTier.ps1 -Path D:\Games\gamecube -ScoreCsv .\gamecube-scores.csv -Extension .rvz,.iso -WhatIf .EXAMPLE .\Move-RomToQualityTier.ps1 -Path D:\Games\ps2 -ScoreCsv .\ps2-scores.csv -UnratedFolder '6 Unrated' | Export-Csv sorted.csv -NoTypeInformation #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] [string]$Path, [Parameter(Mandatory)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Leaf })] [string]$ScoreCsv, [string[]]$Extension, [ValidateCount(4, 4)] [ValidateRange(0, 100)] [int[]]$Threshold = @(90, 80, 70, 60), [ValidateCount(5, 5)] [ValidateNotNullOrEmpty()] [string[]]$TierName = @('1 Must-Play', '2 Great', '3 Good', '4 Average', '5 Niche'), [string]$UnratedFolder ) function Get-MatchKey([string]$Text) { ' ' + ($Text.ToLowerInvariant() -replace '[\u0027\u2019]', '' -replace '&', 'and' -replace '[^a-z0-9]+', ' ').Trim() + ' ' } for ($i = 1; $i -lt $Threshold.Count; $i++) { if ($Threshold[$i] -ge $Threshold[$i - 1]) { throw "Thresholds must go from highest to lowest, like 90,80,70,60." } } $scores = foreach ($row in Import-Csv -LiteralPath $ScoreCsv) { $score = 0 if ($row.Title -and [int]::TryParse(($row.Score -as [string]).Trim(), [ref]$score) -and $score -ge 0 -and $score -le 100) { [pscustomobject]@{ Title = $row.Title; Key = (Get-MatchKey $row.Title); Score = $score } } elseif ($row.Title) { Write-Warning "Skipping CSV row '$($row.Title)': score '$($row.Score)' isn't a number from 0 to 100." } } Write-Verbose "Loaded $(@($scores).Count) scored titles from $ScoreCsv" $extFilter = @($Extension | Where-Object { $_ } | ForEach-Object { if ($_ -like '.*') { $_.ToLower() } else { ".$_".ToLower() } }) $files = Get-ChildItem -LiteralPath $Path -File | Where-Object { -not $extFilter -or $extFilter -contains $_.Extension.ToLower() } foreach ($file in $files) { # Strip a leading genre code like (PLAT) and everything from the first bracket, to get the title. $title = (($file.BaseName -replace '^\([A-Z]{3,4}\)\s*', '') -replace '\s*[\(\[].*$', '').Trim() $key = Get-MatchKey $title $match = $scores | Where-Object { $key.Contains($_.Key) } | Sort-Object { $_.Key.Length } -Descending | Select-Object -First 1 $result = [ordered]@{ Name = $file.Name; MatchedTitle = $null; Score = $null; Tier = $null; Status = 'Unrated' } $folder = $null if ($match) { $result.MatchedTitle = $match.Title $result.Score = $match.Score $index = @($Threshold | Where-Object { $match.Score -lt $_ }).Count $folder = $TierName[$index] $result.Tier = $folder } elseif ($UnratedFolder) { $folder = $UnratedFolder; $result.Tier = $UnratedFolder } if (-not $folder) { [pscustomobject]$result; continue } $destFolder = Join-Path $Path $folder $dest = Join-Path $destFolder $file.Name if (Test-Path -LiteralPath $dest) { $result.Status = 'SkippedExists' Write-Warning "$($file.Name) is already in '$folder'. Left alone." [pscustomobject]$result; continue } $result.Status = 'WhatIf' if ($PSCmdlet.ShouldProcess($file.FullName, "Move to $folder")) { try { if (-not (Test-Path -LiteralPath $destFolder)) { New-Item -ItemType Directory -Path $destFolder -ErrorAction Stop | Out-Null } Move-Item -LiteralPath $file.FullName -Destination $dest -ErrorAction Stop $result.Status = 'Moved' } catch { $result.Status = "Failed: $($_.Exception.Message)" Write-Warning "$($file.Name): $($_.Exception.Message)" } } [pscustomobject]$result }