Last active
May 6, 2026 20:37
-
-
Save Venipa/0b02c79923014727859e23c06fe27c81 to your computer and use it in GitHub Desktop.
script for organizing music files into [album name]
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
| <# | |
| MIT License | |
| Copyright (c) 2026 | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this script and associated documentation files (the "script"), to deal | |
| in the script without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the script, and to permit persons to whom the script is | |
| furnished to do so. | |
| #> | |
| function Get-PreferredBinaryPath { | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| [string]$BinaryName | |
| ) | |
| $chocoInstall = $env:ChocolateyInstall | |
| if (-not [string]::IsNullOrWhiteSpace($chocoInstall)) { | |
| $candidatePath = Join-Path $chocoInstall "bin\$BinaryName.exe" | |
| if (Test-Path -LiteralPath $candidatePath) { | |
| return $candidatePath | |
| } | |
| } | |
| return $BinaryName | |
| } | |
| function Exit-WithAnyKey { | |
| Write-Host "Press any key to exit..." | |
| [void][System.Console]::ReadKey($true) | |
| exit | |
| } | |
| if ($args.Count -eq 0) { | |
| Write-Host "No files provided. Exiting." -ForegroundColor Red | |
| exit 1 | |
| } | |
| $NO_ALBUM_FOLDER = "No Album" | |
| $ffprobePath = Get-PreferredBinaryPath -BinaryName "ffprobe" | |
| function ConvertTo-SafeFolderName { | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| [string]$Name | |
| ) | |
| $safeName = $Name | |
| foreach ($invalidChar in [System.IO.Path]::GetInvalidFileNameChars()) { | |
| $safeName = $safeName.Replace($invalidChar, "_") | |
| } | |
| return $safeName.Trim() | |
| } | |
| function Get-MusicTagsFromMetadata { | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| [string]$FilePath, | |
| [Parameter(Mandatory = $true)] | |
| [string]$ProbePath | |
| ) | |
| $tagMap = @{ | |
| artist = "" | |
| album = "" | |
| title = "" | |
| } | |
| try { | |
| $tagOutput = & $ProbePath -v error -show_entries format_tags=artist,album,title -of default=noprint_wrappers=1:nokey=0 $FilePath | |
| foreach ($line in $tagOutput) { | |
| if ($line -match "^(?:TAG:)?(?<key>[^=]+)=(?<value>.*)$") { | |
| $key = $matches["key"].Trim().ToLowerInvariant() | |
| $value = $matches["value"].Trim() | |
| if ($tagMap.ContainsKey($key) -and -not [string]::IsNullOrWhiteSpace($value)) { | |
| $tagMap[$key] = $value | |
| } | |
| } | |
| } | |
| } catch { | |
| Write-Warning "Failed reading metadata for '$FilePath'." | |
| } | |
| return $tagMap | |
| } | |
| function Get-UniqueDestinationPath { | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| [string]$DirectoryPath, | |
| [Parameter(Mandatory = $true)] | |
| [string]$FileName | |
| ) | |
| $candidatePath = Join-Path $DirectoryPath $FileName | |
| if (-not (Test-Path -LiteralPath $candidatePath)) { | |
| return $candidatePath | |
| } | |
| $baseName = [System.IO.Path]::GetFileNameWithoutExtension($FileName) | |
| $extension = [System.IO.Path]::GetExtension($FileName) | |
| $index = 1 | |
| do { | |
| $candidateName = "$baseName ($index)$extension" | |
| $candidatePath = Join-Path $DirectoryPath $candidateName | |
| $index++ | |
| } while (Test-Path -LiteralPath $candidatePath) | |
| return $candidatePath | |
| } | |
| foreach ($filepath in $args) { | |
| if (-not (Test-Path -LiteralPath $filepath)) { | |
| Write-Warning "File not found: $filepath" | |
| continue | |
| } | |
| $file = Get-Item -LiteralPath $filepath | |
| if ($file.PSIsContainer) { | |
| Write-Warning "Skipping directory: $($file.FullName)" | |
| continue | |
| } | |
| $tagMap = Get-MusicTagsFromMetadata -FilePath $file.FullName -ProbePath $ffprobePath | |
| $rawAlbumName = [string]$tagMap["album"] | |
| $safeAlbumName = ConvertTo-SafeFolderName -Name $rawAlbumName | |
| if ([string]::IsNullOrWhiteSpace($safeAlbumName)) { | |
| $safeAlbumName = $NO_ALBUM_FOLDER | |
| } | |
| $destinationDirectory = Join-Path $file.DirectoryName $safeAlbumName | |
| if (-not (Test-Path -LiteralPath $destinationDirectory)) { | |
| [void](New-Item -ItemType Directory -Path $destinationDirectory) | |
| } | |
| $destinationPath = Get-UniqueDestinationPath -DirectoryPath $destinationDirectory -FileName $file.Name | |
| if ($file.FullName -eq $destinationPath) { | |
| Write-Host "Already organized: $($file.FullName)" -ForegroundColor Yellow | |
| continue | |
| } | |
| Move-Item -LiteralPath $file.FullName -Destination $destinationPath | |
| Write-Host "Moved '$($file.Name)' -> '$safeAlbumName'" -ForegroundColor Green | |
| } | |
| Exit-WithAnyKey |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
usage:
organize-albums.c.ps1 <file1> <file2> <...files>