Scripting Games – Puzzle 1

The first puzzle called for a one-liner that produces the following output:

PSComputerName ServicePackMajorVersion Version  BIOSSerial                                
-------------- ----------------------- -------  ----------                                
win81                                0 6.3.9600 VMware-56 4d 09 1 71 dd a9 d0 e6 46 9f

and set the following challenges:

  • Try to use no more than one semicolon total in the entire one-liner
  • Try not to use ForEach-Object or one of its aliases
  • Write the command so that it could target multiple computers (no error handling needed) if desired
  • Want to go obscure? Feel free to use aliases and whatever other shortcuts you want to produce a teeny-tiny one-liner.

Now, I will be the first to admit that one-liners are not my strong point. My scripts tend to be pretty verbose; I don’t often use aliases, and even after several years of using PowerShell I still stumble when reading code like this:

gc computers.txt | % {gwmi Win32_operatingSystem | ? {$_.Caption -like '*8.1*'}}

So, with verbosity in mind and with the realisation that at least two WMI classes would need to be queried to solve this puzzle, I first thought about how I might solve this if I wasn’t restricted to one line of code. The solution is pretty straight forward:

Query the two WMI classes, make a custom object with the required properties and then output the object.

$os = Get-WmiObject Win32_OperatingSystem
$bios = Get-WmiObject Win32_BIOS

$obj = [PSCustomObject]@{
    PSComputerName = $os.PSComputerName
    ServicePackMajorVersion = $os.ServicePackMajorVersion
    Version = $os.Version
    BIOSSerial = $bios.SerialNumber
}

Write-Output $obj

The next step was to condense this down into one line of code.

Because of the challenge to use no more than one semicolon, I was keen to try and avoid using semicolons to separate the statements. The alternative I found, which at the time I thought was pretty clever, was to separate the commands using parentheses and commas.
What I later realised is that what this is doing is generating an array where each array member is the result of the command run in the parentheses. This is why I had to use Out-Null to suppress PowerShell’s desire output all three elements to the display.

(($os = Get-WmiObject Win32_OperatingSystem),($bios = Get-WmiObject Win32_BIOS) | Out-Null),
    ([PSCustomObject]@{
        PSComputerName = $os.PSComputerName;
        ServicePackMajorVersion = $os.ServicePackMajorVersion;
        Version = $os.Version;
        BIOSSerial = $bios.SerialNumber})

So, I was down to one line but still had too many semicolons. Next up was to find an alternative way to create the object. Rather than use the [PSCustomObject] accelerator, I opted for the old-school cmdlet New-Object and its friend Add-Member. Format-Table was used to format the output exactly as specified in the puzzle, without it there were too many tabs.

(($os = Get-WmiObject Win32_OperatingSystem),($bios = Get-WmiObject Win32_BIOS) | Out-Null),
    (New-Object -TypeName PSObject |
     Add-Member -MemberType NoteProperty -Name PSComputerName -Value $os.PSComputerName -PassThru |
     Add-Member -MemberType NoteProperty -Name ServicePackMajorVersion -Value $os.ServicePackMajorVersion -PassThru |
     Add-Member -MemberType NoteProperty -Name Version -Value $os.Version -PassThru |
     Add-Member -MemberType NoteProperty -Name BIOSSerial -Value $bios.SerialNumber -PassThru |
     Format-Table -AutoSize)

The next step was to use aliases to reduce the amount of text. I also took advantage of PowerShell’s ability to recognise parameters using the fewest number of unique characters to shorten them as far as possible:

(($os = gwmi Win32_OperatingSystem),($bios = gwmi Win32_BIOS) | Out-Null),
    (New-Object -t PSObject |
     Add-Member -m NoteProperty -na PSComputerName -va $os.PSComputerName -pa |
     Add-Member -m NoteProperty -na ServicePackMajorVersion -va $os.ServicePackMajorVersion -pa |
     Add-Member -m NoteProperty -na Version -va $os.Version -pa |
     Add-Member -m NoteProperty -na BIOSSerial -va $bios.SerialNumber -pa |
    ft -a)

Finally, I had to handle multiple computers. I opted for Read-Host for this, and just used a while loop to enable the user to keep entering computer names until they get bored and use ctrl + c to exit.

while ($true){
     ($computer = Read-Host 'Enter a computer name or ctrl + c to quit'),
        (($os = gwmi Win32_OperatingSystem -co $computer),($bios = gwmi Win32_bios -co $computer) | Out-Null),
            (New-Object -t PSObject | Add-Member -m NoteProperty -na PSComputerName -va $os.PSComputerName -pa |
             Add-Member -m NoteProperty -na ServicePackMajorVersion -va $os.ServicePackMajorVersion -pa |
             Add-Member -m NoteProperty -na Version -va $os.Version -pa |
             Add-Member -m NoteProperty -na BIOSSerial -va $bios.SerialNumber -pa | ft -a)}

There is no doubt that this is a very awkward solution to a fairly simple problem; clearly it’s not the kind of solution that the puzzle setter had in mind. That said, with the exception of its phenomenal length, it does meet the brief :).

Note: All code can be entered on a single line with out pressing ENTER until you’ve typed the last character, I’ve wrapped the lines at natural points to aid readability.

Posted in Uncategorized | Leave a comment

The Scripting Games is Back!

On 27th June the new format for the Scripting Games was announced on the Hey, Scripting Guy! blog. One week later, powershell.org published the first puzzle.

You’ll find full instructions on how to submit your solution on PowerShell.org.

Posted in PowerShell | Tagged , | Leave a comment

Using PowerShell to Repair Orphaned Tracks in a Windows Media Player Library

When you rip a CD into Windows Media Player (WMP), WMP will attempt to retrieve information about the album from the Internet. I have encountered a problem with this where, sometimes, all of the album information is retrieved except for the information for the first track. Instead of showing the correct track name and associated album, the track is orphaned as ‘Track 1’ belonging to ‘Unknown Album (DD/MM/YY HH/MM/SS)’.

When ripping music, for a single album, this is trivial, a minor inconvenience quickly fixed with a manual edit. However, if WMP rebuilds the media library, such as when you move your music collection to a different partition, you may be left with hundreds of orphaned tracks. The reason for this is that WMP repopulates the library using the meta data for the music file. If you view the meta data by opening the Details tab for the file in Windows Explorer you will see that it’s incorrect.

In this article I will show how the information for the track can be retrieved from the full filename and then used to populate the information in the WMP library, this will ensure that the tracks are displayed correctly when viewed in WMP.

PowerShell provides no native cmdlets for controlling WMP or for manipulating the contents of the media library. However, we can use a COM object provided by the WMP ActiveX control. The WMP object model is comprehensively documented at:
http://msdn.microsoft.com/en-us/library/windows/desktop/dd563945(v=vs.85).aspx

The first thing to do is to create an instance of a Player COM object:

$wmp = New-Object -ComObject WMPlayer.ocx

Like all objects in PowerShell, we can inspect the available properties and methods of this object by piping it to Get-Member. A sample of the output is shown below.

$wmp | Get-Member

Name               MemberType Definition
----               ---------- ----------
close              Method     void close ()
launchURL          Method     void launchURL (string)
cdromCollection    Property   IWMPCdromCollection cdromCollection () {get}
currentMedia       Property   IWMPMedia currentMedia () {get} {set}
currentPlaylist    Property   IWMPPlaylist currentPlaylist () {get} {set}
mediaCollection    Property   IWMPMediaCollection mediaCollection () {get}
playerApplication  Property   IWMPPlayerApplication playerApplication () {get}

The output from Get-Member shows us that the $wmp object has a mediaCollection property. The mediaCollection property is itself an object and we can pipe this object to Get-Member to explore its properties and methods.

$wmp.mediaCollection | Get-Member

Name                         MemberType Definition
----                         ---------- ----------
getAll                       Method     IWMPPlaylist getAll ()
getAttributeStringCollection Method     IWMPStringCollection getAttributeStringCollection (string, string)
getByAlbum                   Method     IWMPPlaylist getByAlbum (string)
getByAttribute               Method     IWMPPlaylist getByAttribute (string, string)
getByAuthor                  Method     IWMPPlaylist getByAuthor (string)
getByGenre                   Method     IWMPPlaylist getByGenre (string)
getByName                    Method     IWMPPlaylist getByName (string)

We can see that the mediaCollection object provides several methods that could be used to query the library. Our orphaned tracks are missing or have incorrect data for Author, Genre, and Album but do we know that they’re consistently named ‘Track 1’ so we will use the getByName() method.

The getbyName() method will return a Playlist object containing all of the media items in the library called ‘Track 1’.

$playList = $wmp.mediaCollection.getByName('Track 1')

Each item in the playlist is referenced by an index number, starting at 0. So to assign the first item in the playlist to the variable $item we would use:

$item = $playList.Item(0)

To access each item in the playlist in turn we can use a for loop to increment the index number.

for ($i = 0; $i -lt $playList.count; $i++) {

    $item = $playList.Item($i)
    … do more stuff …

}

All of the information about the track that we want to update can be obtained from its full filename. The full filename is stored in the sourceURL attribute. This is accessed using the getItemInfo() method which accepts an attribute name as an argument.

$sourceURL = $item.getItemInfo("sourceURL")

More information about media item attributes can be found here:
http://msdn.microsoft.com/en-us/library/windows/desktop/dd563866(v=vs.85).aspx

To obtain the track name we can use Split-Path to get the filename from the sourceURL.

$fileName = $sourceURL | Split-Path -Leaf

The filename will have the format ‘nn Track Name.abc’ where nn is the track number and .abc is the file extension of the media file. To set the track name, we only require the ‘Track Name’ substring which we can obtain as follows:

$chars = $filename.Length -7
$trackName = $fileName.Substring(3,$chars)

The album name is the parent folder of the media file which we can obtain by splitting the path twice.

$albumName = $sourceURL | Split-Path | Split-Path -leaf

The artist name is the parent folder of the album folder which we can obtain by splitting the path three times.

$artistName = $sourceURL | Split-Path | Split-Path | Split-Path -leaf

One situation where this does not have the desired result is where the album is by various artists as this will result in the Artist attribute being set to ‘Various Artists’ rather than the actual artist that performed that particular track.

Once we have the required information we can update the item’s information using the setItemInfo() method which we call with two arguments, the name of the attribute that we want to update and the new value for the attribute.

$item.setItemInfo("Name",$trackName)
$item.setItemInfo("Album",$albumName)
$item.setItemInfo("Artist",$artistName)

In the final version of the script, I wrapped getting and setting of the information in a try catch block in case I was left with any tracks that could not be updated. I also chose not to process any items where the filename included the string ‘Unknown Artist’. If WMP has stored the album as ‘Unknown Artist’ it was unable to get information for that CD from the Internet, therefore the information in the file name is not correct and it’s pointless updating the library.

Finally, during testing, I found that the library does not always update straight away.
There is mention made of this in the documentation which states that “If you write code using the Windows Media Player control to change the value of an existing read/write attribute in a media item that has been added to the library, the effect is nearly the same as if the user had modified the attribute using Windows Media Player. The value is written to the library database and at some indeterminate time the database synchronizes with the file.”.
Releasing the COM object, with the final line of code, seems to consistently speed up this process.

The finished script:

# Note: Close any open instances of Windows Media Player before running the script

# Create an instance of a Player object

$wmp = New-Object -ComObject WMPlayer.ocx

# Generate a Playlist object containing all media items named 'Track 1'

$playList = $wmp.mediaCollection.getByName('Track 1')

for ($i = 0; $i -lt $playList.count; $i++) {

    try {

    # Get the sourceURL (full filename) of the current item

    $item = $playList.Item($i)
    $sourceURL = $item.getItemInfo("sourceURL")

    # Exclude items where the filename includes unknown artist to prevent processing
    # of unknown albums

        if ($sourceURL -notlike '*Unknown Artist*') {

            # Obtain the correct information for current item

            $fileName = $sourceURL | Split-Path -Leaf
            $chars = $filename.Length -7
            $trackName = $fileName.Substring(3,$chars)
            $albumName = $sourceURL | Split-Path | Split-Path -leaf
            $artistName = $sourceURL | Split-Path | Split-Path | Split-Path -leaf

            # Update the library information for the current item

            $item.setItemInfo("Name",$trackName)
            $item.setItemInfo("Album",$albumName)
            $item.setItemInfo("Artist",$artistName)

        } #end if
    } #end try

    catch {

        Write-Output "Error processing $playlist.Item($i). Track: $trackName, Album: $albumName, Artist: $artistName"
    } #end catch
} #end for

#Release the COM Object

[System.Runtime.Interopservices.Marshal]::ReleaseComObject($wmp) | Out-Null

Post Script
When I originally wrote this article I had not found a way to update the file meta data so although I now had a way to repair the orphaned tracks by updating the information in the Windows Media Player database the underlying problem still remained. During further research I found a library, TagLib that can be used to write file meta data. In a future article I’ll look at how that library can be used with PowerShell to update the file meta data.

Posted in PowerShell, Windows, Windows Media Player | Tagged , , | Leave a comment