<# .SYNOPSIS Moves movies and TV show folders into flat A-Z "bucket" folders. .DESCRIPTION Looks at every top-level folder and video file in a source folder and moves it into a bucket folder under the destination, based on the first character of its name: A through Z, "0-9" for titles that start with a number, and "#" for everything else. Leading articles ("The", "A", "An") can be ignored so "The Wire" lands in W. Nothing is overwritten. If the target already exists, the item is skipped and reported. Supports -WhatIf and -Confirm, returns one object per item, and can log to CSV. .PARAMETER SourcePath The messy folder to sort. Only its top-level items are moved; what's inside a show or movie folder is left alone. .PARAMETER DestinationPath Where the bucket folders live. Can be the same as SourcePath to sort a folder in place. .PARAMETER Scheme Letter gives one bucket per letter (A, B, C...). Range gives five wider buckets (A-E, F-J, K-O, P-T, U-Z) for smaller libraries. .PARAMETER IgnoreArticle Skip a leading "The", "A" or "An" when picking the bucket. The name itself isn't changed. .PARAMETER Extension Loose files with these extensions are treated as movies. Anything else is left in place. .PARAMETER LogPath Optional CSV file for the results. Written even during -WhatIf, so you can review the plan. .EXAMPLE .\Move-MediaToBucket.ps1 -SourcePath D:\Incoming\TV -DestinationPath D:\TV -IgnoreArticle -WhatIf .EXAMPLE .\Move-MediaToBucket.ps1 -SourcePath D:\Movies -DestinationPath D:\Movies -LogPath .\movies.csv #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] [string]$SourcePath, [Parameter(Mandatory)] [string]$DestinationPath, [ValidateSet('Letter', 'Range')] [string]$Scheme = 'Letter', [switch]$IgnoreArticle, [string[]]$Extension = @('.mkv', '.mp4', '.m4v', '.avi'), [string]$LogPath ) function Get-BucketName { param([string]$Name) $sortName = $Name.Trim() if ($IgnoreArticle) { $sortName = $sortName -replace '^(the|an|a)\s+', '' } $sortName = $sortName.TrimStart("'", '"', '(', '[', '.', '-', '_', ' ') if (-not $sortName) { return '#' } # "Élite" should land in E, so strip accents before looking at the first letter. $first = ([string]$sortName[0]).Normalize([Text.NormalizationForm]::FormD)[0] $first = [char]::ToUpperInvariant($first) if ($first -ge [char]'0' -and $first -le [char]'9') { return '0-9' } if ($first -lt [char]'A' -or $first -gt [char]'Z') { return '#' } if ($Scheme -eq 'Letter') { return [string]$first } foreach ($range in 'A-E', 'F-J', 'K-O', 'P-T', 'U-Z') { if ($first -ge $range[0] -and $first -le $range[2]) { return $range } } } $source = (Resolve-Path -LiteralPath $SourcePath).ProviderPath.TrimEnd('\', '/') $destination = [IO.Path]::GetFullPath($DestinationPath).TrimEnd('\', '/') $bucketNames = @('0-9', '#') + $(if ($Scheme -eq 'Letter') { [char[]](65..90) | ForEach-Object { [string]$_ } } else { 'A-E', 'F-J', 'K-O', 'P-T', 'U-Z' }) $sortInPlace = $source -eq $destination $items = Get-ChildItem -LiteralPath $source | Where-Object { if ($_.PSIsContainer) { # Don't try to sort the buckets themselves, or the destination if it sits inside the source. -not ($sortInPlace -and $bucketNames -contains $_.Name) -and $_.FullName.TrimEnd('\', '/') -ne $destination } else { $Extension -contains $_.Extension.ToLowerInvariant() } } $results = foreach ($item in $items) { $bucket = Get-BucketName -Name $item.Name $target = Join-Path (Join-Path $destination $bucket) $item.Name $result = [pscustomobject]@{ Name = $item.Name Type = if ($item.PSIsContainer) { 'Folder' } else { 'File' } Bucket = $bucket Status = $null Source = $item.FullName Destination = $target Message = $null } if (Test-Path -LiteralPath $target) { $result.Status = 'Skipped' $result.Message = 'Something with this name is already in the bucket.' Write-Warning "Skipped '$($item.Name)': already exists in $bucket" } elseif ($PSCmdlet.ShouldProcess($item.FullName, "Move to $bucket")) { try { $bucketPath = Join-Path $destination $bucket if (-not (Test-Path -LiteralPath $bucketPath)) { New-Item -Path $bucketPath -ItemType Directory -Force -ErrorAction Stop | Out-Null } Move-Item -LiteralPath $item.FullName -Destination $target -ErrorAction Stop $result.Status = 'Moved' Write-Verbose "Moved '$($item.Name)' to $bucket" } catch { $result.Status = 'Error' $result.Message = $_.Exception.Message Write-Warning "Failed on '$($item.Name)': $($_.Exception.Message)" } } else { $result.Status = if ($WhatIfPreference) { 'WhatIf' } else { 'Declined' } } $result } if ($LogPath -and $results) { $results | Export-Csv -LiteralPath $LogPath -NoTypeInformation -WhatIf:$false -Confirm:$false } $results