So my wife just swapped her iPhone 4 for a new HTC Titan Windows Phone thanks to the recent Smoked by Windows Phone challenge. The first thing she asked me about is how to get all of her music over to the new phone. I’ve been using Zune Pass quite happily for a while however she has still continued to buy music from iTunes. Her library, combined with the one that I had been amassing over the last several yeas before Zune Pass, meant that we have quite a bit of owned music. Unfortunately like my pictures in the previous blog post thanks to multiple system backups, multiple computers, and the general fact that most of my files are in a sad state of unorganized I’m left with no consolidated music library.
Powershell to the rescue!!
I took the basic idea from the picture script I posted earlier and combined it with an open source DLL I found online which can query the metadata from a music file. Grab the latest Windows package from http://download.banshee.fm/taglib-sharp/ and extract the taglib-sharp.dll file into the same folder as your powershell script.
Here is the script – it will remove duplicates and create a new folder with all of your music across multiple locations containing the unique files in the directory hierarchy ARTIST\ALBUM\TITLE.
#This is really divided into two scripts. The first one searches for our audio files and creates and MD5 hash of each one. #Merging them all into the same folder allows us to eleminate duplicates. #The second script reads the audio tags and puts them back out into folders based on artist, album, and title. #Function to calculate the MD5 hash of a file function Get-MD5([System.IO.FileInfo] $file = $(throw 'Usage: Get-MD5 [System.IO.FileInfo]')) { # This Get-MD5 function sourced from: # http://blogs.msdn.com/powershell/archive/2006/04/25/583225.aspx $stream = $null; $cryptoServiceProvider = [System.Security.Cryptography.MD5CryptoServiceProvider]; $hashAlgorithm = new-object $cryptoServiceProvider $stream = $file.OpenRead(); $hashByteArray = $hashAlgorithm.ComputeHash($stream); $stream.Close(); ## We have to be sure that we close the file stream if any exceptions are thrown. trap { if ($stream -ne $null) { $stream.Close(); } break; } $md5 = "" foreach ($byte in $hashByteArray) { $md5 = $md5 + $byte.ToString("X2"); } return $md5; } #Figure out where we are at and if there is a subfolder called output. If not we will create one. This is where we will put all of our images. $currdir = split-path -parent $MyInvocation.MyCommand.Definition $outputdir = $currdir + "\output\" if (!(Test-Path -path $outputdir)) { New-Item $outputdir -type directory } $tmpdir = $currdir + "\tmp\" if (!(Test-Path -path $tmpdir)) { New-Item $tmpdir -type directory } #Using Get-ChildItem we search for all files matching our extension recurisvley from the location of the script down. $files = Get-ChildItem -Exclude $outputdir -Recurse -Include *.m4a,*.mp3,*.wma #We're going to keep track of how many files we process and put a unique number in the file for each one (eliminates all possibility of duplicate filename) $i = 0; foreach($f in $files) { #Increment our counter $i++; #Calcualte the MD5 of the original file so that we can look for duplicates later $md5 = Get-MD5($f); #The target filename will be the output directory with the MD5 hash as the filename and the original file extension $targetname = $tmpdir + $md5 + $f.extension; #Write the file to the output folder, if there are duplicate files Copy-Item's default behavior is to overwrite. This eleminates the dupes. Copy-Item $f.fullname $targetname #Could delete the old version, I'm leaving it as a backup so I've commented this out. #Remove-Item $f.fullname; Write-Output $targetname #Write percent compelted of current operation $percent = [System.Math]::Round((($i / $files.Count) * 100), 2) Write-Progress -Activity "Calculating hashes..." -PercentComplete $percent -CurrentOperation "$percent% complete" -Status "Please wait." } #Now that we've zapped our duplicates we can move the files out to their target locations #We loop through them again extracting the important bits from the audio tags #Format will be /$artist/$album/$title.Extension #Load the taglib library assuming the DLL is in the same folder (be sure to Unblock it). #You can get the DLL from http://download.banshee.fm/taglib-sharp/ -- I'm using 2.0.4.0 which was latest at time of writing this [Reflection.Assembly]::LoadFile( (Resolve-Path ".\taglib-sharp.dll") ) #List of characters that we can't use $invalid_characters = "[{0}]" -f ([Regex]::Escape([String][System.IO.Path]::GetInvalidPathChars()) + "/", "\", "*", "?", ":") $files = Get-ChildItem $tmpdir -Recurse $i = 0; foreach($f in $files) { $i++; #Load up the audio file into TagLib $audiofile = [TagLib.File]::Create($f.fullname); if($audiofile.Tag.AlbumArtists) { $artist = [string] $audiofile.Tag.AlbumArtists } elseif ($audiofile.Tag.FirstArtist) { $artist = [string] $audiofile.Tag.FirstArtist $audiofile.Tag.AlbumArtists = $artist $audiofile.Save() } else { $artist = "Unknown" } if($audiofile.Tag.Album) { $album = [string] $audiofile.Tag.Album } else { $album = "Unknown" } if ($audiofile.Tag.Title) { $title = [string] $audiofile.Tag.Title } else { $title = "Unknown" } $artist = [string][Regex]::Replace($artist, $invalid_characters, '') $album = [string][Regex]::Replace($album, $invalid_characters, '') $title = [string][Regex]::Replace($title, $invalid_characters, '') #Where are we putting the new file? $targetname = $outputdir + $artist + "\" + $album + "\" + $title #Make sure that our folders exist (one for each month under the year) and if not create them if (!(Test-Path -path ($outputdir + $artist))) { $output = New-Item ($outputdir + $artist) -type directory } if (!(Test-Path -path ($outputdir + $artist + "\" + $album))) { $output = New-Item ($outputdir + $artist + "\" + $album) -type directory } $dupe = $true $x = 1 While($dupe) { $x++ if (!(Test-Path -Path ($targetname + $f.extension))) { $targetname = $targetname + $f.extension $dupe = $false } elseif (!(Test-Path -Path ($targetname + "-" + $x + $f.extension))) { $targetname = $targetname + "-" + $x + $f.extension $dupe = $false } } #Move the source file to it's new home. Move-Item -Path $f.fullname -Destination $targetname Write-Output $targetname #Write percent compelted of current operation $percent = (($i / $files.Count) * 100) Write-Progress -Activity "Moving files..." -PercentComplete $percent -CurrentOperation "$percent% complete" -Status "Please wait." }





