Downloading Single or Entire Ranges of Lego Set Instructions with PowerShell

Update 20/07/2015:

The details in this post are now superseded by a post I have made using the Brickset API.

-———————————————————————————————————————

I’ve always enjoyed Lego and it’s currently experiencing a resurgence in our house thanks to strategic hinting encouragement that my children would find it fun too. (It seems like I’m not the only one)

What does tend to happen though is that as sets are pulled apart played with we often need to dig out the instructions to put entire sets pieces back together again. Although I carefully file any instructions from sets we purchase, often it’s easier to download them as pdfs from the Lego website and view them on a tablet and sometimes we buy them 2nd hand without instructions so need to download them anyway.

The official Lego website is pretty good for making these available, even for sets which were released a long time ago, but I particularly like BrickInstructions.com since they make it very easy to find links on the official Lego site to the particular instructions you need and also display the instructions on their own site.

As our collection of sets grew I was manually downloading a copy of the instructions for each one to store for quick reference, then I figured why not just download the whole lot for the particular ranges we are interested in, so that I’ve always got any of them. Naturally I was not going to do that manually……

So the following two PowerShell functions will enable you to download either single, multiple or an entire range of Lego set instructions via the links provided by BrickInstructions.com. The download requests are submitted via a single BITS Transfer job so make good use of available bandwidth, handle network interruptions etc…

Note: Some of the code in these functions requires PowerShell v3

If you already know the Lego set numbers you can use the first function Get-LegoSetInstructions like this:


9488,7655,7656 | Get-LegoSetInstructions -DestinationFolder "C:\\Lego\\Instructions\\Star Wars" -Verbose

The files hosted on  the Lego website are named with a generic number code, so I change this with each download to the name and number of the set so that they are easy to find once downloaded:


function Get-LegoSetInstructions { <# .SYNOPSIS Function to download Lego Set Instructions.

.DESCRIPTION Function to download Lego Set Instructions.

.PARAMETER ID Lego set to download instructions for

.PARAMETER Link Link to Lego set instructions page

.PARAMETER DestinationFolder Folder to save the instructions in

.PARAMETER Force Overwrite an existing instructions file if it already exists

.INPUTS String

.OUTPUTS None.

.EXAMPLE PS> Get-LegoSetInstructions -ID 9488 -DestinationFolder "C:\\Lego\\Instructions"

.EXAMPLE PS> 9488,7655,7656 | Get-LegoSetInstructions -DestinationFolder "C:\\Lego\\Instructions"

.EXAMPLE PS> Get-LegoSetInstructions -Link "http://lego.brickinstructions.com/instructions.php?code=10212&amp;set=Imperial Shuttle" -DestinationFolder "C:\\Lego\\Instructions"

.NOTES Version: 1.0 - First draft Date: 17/04/2013 Tag: lego,download,BITS #> \[CmdletBinding(DefaultParametersetName="ID")\]

Param (

\[parameter(Mandatory=$true,ValueFromPipeline=$true,ParameterSetName="ID")\] \[ValidateNotNullOrEmpty()\] \[String\[\]\]$ID,

\[parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName="Link")\] \[ValidateNotNullOrEmpty()\] \[String\[\]\]$Link,

\[parameter(Mandatory=$true)\] \[ValidateNotNullOrEmpty()\] \[String\]$DestinationFolder,

\[parameter(Mandatory=$false)\] \[Switch\]$Force )

begin {

$ErrorActionPreference = 'Stop' Write-Debug $MyInvocation.MyCommand.Name $LegoSetObject = @()

\# --- Test for the existence of the BITS transfer module if (!(Get-Module BitsTransfer -ListAvailable)){

throw "BITS Transfer module not available. Run from a system with this module" }

\# --- Test for the existence of the Destination Folder if (!(Test-Path $DestinationFolder)){

throw "Destination Folder $DestinationFolder does not exist" } }

process {

switch ($PsCmdlet.ParameterSetName) { "ID" {

foreach ($Set in $ID){

\# --- Retrieve the web page for the Lego instructions $url = Invoke-WebRequest "http://lego.brickinstructions.com/instructions.php?code=$Set"

\# --- Find links that contain pdfs. There may be multiple for different print formats on newer modelss if ($PDFLink = $url.Links | Where-Object {$\_.outerHTML -like '\*pdf\*'} | Select-Object -First 1){

$Description = $url.ParsedHtml.nameProp $Output = Join-Path -Path $DestinationFolder -Childpath ($Description + ".pdf")

$Object = \[pscustomobject\]@{

LegoSet = $Set Description = $Description DownloadLink = $PDFLink.href Output = $Output } $LegoSetObject += $Object

} else { Write-Warning "No instructions for Lego Set $Set were found" } } }

"Link" {

foreach ($href in $Link){

\# --- Retrieve the web page for the Lego instructions $url = Invoke-WebRequest $href

\# --- Pick out the set number from the link $href -match ".\*?(?<Set>\\d+)" | Out-Null $Set = $Matches.Set

\# --- Find links that contain pdfs. There may be multiple for different print formats on newer modelss if ($PDFLink = $url.Links | Where-Object {$\_.outerHTML -like '\*pdf\*'} | Select-Object -First 1){

$Description = $url.ParsedHtml.nameProp $Output = Join-Path -Path $DestinationFolder -Childpath ($Description + ".pdf")

$Object = \[pscustomobject\]@{

LegoSet = $Set Description = $Description DownloadLink = $PDFLink.href Output = $Output } $LegoSetObject += $Object } else { Write-Warning "No instructions for Lego Set $Set were found" } } } } }

end { # --- If no Force parameter, then filter the downloads to those that don't already exist if (-not ($PSBoundParameters.ContainsKey('Force'))){

$LegoSetObject = $LegoSetObject | Where-Object {-not (Test-Path $\_.Output)} }

\# --- $Job = Start-BitsTransfer -Source $LegoSetObject.DownloadLink -Destination $LegoSetObject.Output -Asynchronous

Write-Verbose "Checking the status of the BITS transfer download job...."

while (($Job.JobState -eq "Transferring") -or ($Job.JobState -eq "Connecting")){

Write-Verbose "BITS Transfer job is still downloading...." Start-Sleep 10}

switch ($Job.JobState){

"Transferred" {Complete-BitsTransfer -BitsJob $Job} "Error" {Write-Warning "BITS transfer has failed..." } default {Write-Warning "Please investigate download for Lego Set "} } Write-Verbose "BITS transfer download job has completed...."

Write-Output $LegoSetObject } }

If you don’t know the set number or want an entire range then you can use the Get-LegoSetLink function to generate links to the pages on the BrickInstructions.com website for all of the Lego sets in that range. The list of possible ranges are on the left-hand side of the site.

So to get the Harry Potter range:


Get-LegoSetLink -Range "Harry Potter"

The Get-LegoSetInstructions function will also take hyperlinks as input, so to get an entire range of instructions downloaded you can do this:


Get-LegoSetLink -Range "Harry Potter" | Get-LegoSetInstructions -DestinationFolder "C:\\Lego\\Instructions\\Harry Potter" -Verbose


function Get-LegoSetLink { <# .SYNOPSIS Function to retrieve Lego Set Links from supplied Lego range.

.DESCRIPTION Function to retrieve Lego Set Links from supplied Lego range.

.PARAMETER Range Range of Lego sets to retrieve links for

.INPUTS String

.OUTPUTS None.

.EXAMPLE PS> Get-LegoSetLink -Range "Star Wars"

.NOTES Version: 1.0 - First draft Date: 18/04/2013 Tag: lego,link #> \[CmdletBinding()\]

Param (

\[parameter(Mandatory=$true,ValueFromPipeline=$true)\] \[ValidateNotNullOrEmpty()\] \[String\]$Range

)

begin {

$ErrorActionPreference = 'Stop' Write-Debug $MyInvocation.MyCommand.Name $LegoLinkObject = @() }

process {

\# --- Retrieve the web page for the Lego instructions $EndUrl = ("lego\_" + ($Range -replace " ", "\_") + ".php").ToLower() $url = Invoke-WebRequest "http://lego.brickinstructions.com/$EndUrl"

\# --- Find the Lego set links if ($LegoSets = $url.links | Where-Object {$\_.href -like '\*set=\*'}){ foreach ($LegoSet in $LegoSets){

$Object = \[pscustomobject\]@{

Description = $LegoSet.innerHTML Link = "http://lego.brickinstructions.com/" + $LegoSet.href } $LegoLinkObject += $Object }

\# If there are additional pages, e.g. for large ranges, cycle through each page to find all of the sets if ($AdditionalPages = $url.links | Where-Object {$\_.href -like '\*page=\*'} | Where-Object {$\_.innerhtml -match "\\d+"} | Select-Object innerhtml,href -Unique){

foreach ($Page in $AdditionalPages){

$AdditionalPageURL = "http://lego.brickinstructions.com" + $Page.href $url = Invoke-WebRequest $AdditionalPageURL

$LegoSets = $url.links | Where-Object {$\_.href -like '\*set=\*'}

foreach ($LegoSet in $LegoSets){

$Object = \[pscustomobject\]@{

Description = $LegoSet.innerHTML Link = "http://lego.brickinstructions.com/" + $LegoSet.href } $LegoLinkObject += $Object } } } } else {

Write-Warning "No sets are available for this range" } } end { Write-Output $LegoLinkObject }

}

Accio Instructions!