Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Sort a Game Collection Into Tiers by Review Score

Moves game files into Must-Play, Great, Good, Average and Niche folders using a CSV of titles and scores, with series fallbacks, so a huge backlog shows you the best stuff first.

AT A GLANCEMove-RomToQualityTier.ps1
What it does
Reads a CSV of titles and 0-100 scores, matches each game file in a folder to its row, and moves the file into a tier folder (1 Must-Play, 2 Great, 3 Good, 4 Average, 5 Niche). Short rows act as series fallbacks. Unmatched games stay put or go to an Unrated folder.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • No modules
  • A CSV with Title and Score columns that you put together
Permissions
Read and write access to the game folder. No admin rights.
Runs on
Windows 10/11, macOS or Linux with PowerShell 7
Tested
Parse-checked and run in PowerShell 7.4 against a folder of empty dummy files, with -WhatIf, for real, with an Unrated folder, genre-tagged names, apostrophes in titles and a series fallback row

Part 9 of the thread Keeping a media library tidy

A full set for any console is hundreds of games, and most of them you'll never play. When I sit down with a system I haven't touched in a while, I want the best twenty in front of me, not an alphabetical wall.

So this script sorts a folder into tiers. You give it a CSV of titles and scores, from Metacritic, your favorite review site, or your own opinions. Anything 90 or above goes in "1 Must-Play", 80s in "2 Great", and so on down. A short row like "Mario Party" works as a fallback for a whole series, and a full title always beats it.

It moves files, it never deletes or overwrites, and -WhatIf shows you the whole plan first.

Move-RomToQualityTier.ps1Download
<#
.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
}

Parameters

ParameterTypeDefaultWhat it's for
-Pathstring—Required. The folder of games. Only files directly in it are sorted.
-ScoreCsvstring—Required. CSV with Title and Score (0 to 100) columns.
-Extensionstring[]every fileOnly sort these extensions.
-Thresholdint[]90, 80, 70, 60The four cut-off scores, highest first.
-TierNamestring[]1 Must-Play ... 5 NicheThe five folder names, best first. The numbers keep them in order in a file browser.
-UnratedFolderstring—A folder for games with no score. Without it, they stay where they are.

Run it

Preview how a GameCube folder would sort.

.\Move-RomToQualityTier.ps1 -Path D:\Games\gamecube -ScoreCsv .\gamecube-scores.csv -Extension .rvz,.iso -WhatIf

See which games the CSV is missing.

.\Move-RomToQualityTier.ps1 -Path D:\Games\gamecube -ScoreCsv .\gamecube-scores.csv -WhatIf | Where-Object Status -eq Unrated

Stricter tiers, with unscored games in their own folder, and keep the list.

.\Move-RomToQualityTier.ps1 -Path D:\Games\ps2 -ScoreCsv .\ps2-scores.csv -Threshold 92,85,75,65 -UnratedFolder '6 Unrated' | Export-Csv sorted.csv -NoTypeInformation

What you'll see

Example outputvalues are illustrative
Name                           MatchedTitle  Score Tier        Status
----                           ------------  ----- ----        ------
(PLAT) Mario Party 4 (USA).rvz Mario Party      75 3 Good      Moved
F-Zero GX (USA).rvz            F-Zero GX        89 2 Great     Moved
Luigi's Mansion (USA).rvz      Luigis Mansion   78 3 Good      Moved
Metroid Prime (USA).rvz        Metroid Prime    97 1 Must-Play Moved
Mystery Game (USA).rvz                                         Unrated
Sonic Heroes (USA).rvz         Sonic            65 4 Average   Moved

How it works

  1. Check the settings. The four thresholds must go from highest to lowest, and there must be exactly five tier names.
  2. Load the scores. Rows with a missing or out-of-range score get a warning and are skipped. Titles are squashed to lowercase letters and numbers so punctuation and apostrophes don't matter.
  3. Get each file's title. A leading genre code like (PLAT) is ignored, and so is everything from the first bracket on.
  4. Find the best row. Every row whose title appears inside the file's title is a candidate, and the longest wins, which is how a series fallback and a specific title live side by side.
  5. Pick the tier. The score is compared with the thresholds: 90 and up is tier 1, 80 to 89 tier 2, and so on.
  6. Move it. The tier folder is created if needed, and the file moves in, unless a file with the same name is already there.

Take it further

  • Tag genres first. With Add-RomGenreTag, the tier folders end up sorted by genre inside, which makes the Must-Play folder a very good menu.
  • Use your own scores. Add a column for your rating and export that as Score instead. Your tiers, not the critics'.
  • Pair it with a cleanup. Set the Niche folder aside with Move-RomByKeyword or just leave it. Nothing here deletes anything.

Things that'll trip you up

  • Scores are only as good as the CSV. The script doesn't look anything up. A score list from a review aggregator is a fine start, but cult favorites tend to land in Average or Niche. Edit the scores to match your own taste; it's your collection.
  • Series rows are blunt. A "Sonic" row puts every Sonic game in the same tier unless a longer row overrides it. Add full titles for the ones that deserve better, or worse.
  • Tier folders move the files. Frontends and emulators pointed at the system folder may not look in subfolders. Check yours scans recursively, or point it at the tier folders.
  • Only the top level is sorted. Files already inside tier folders aren't touched, so running it again after adding games only sorts the new ones. To re-sort everything, move the files back up first.