Building an Xbox Live Widget using PowerShell

Something a little fun that I had the opportunity to work on was building a Xbox Live widget using PowerShell that does a nice job of capturing all of the relevant information that a Xbox live person would like to see along such as online friends, gamer score and the most recent activity.

elgatorabioso

What got me started down this road was a few blog posts from fellow PowerSheller Jonathan Tyler (Twitter | Blog) that described how he found an API for performing queries to gather information on Xbox Live.

The API site is: https://xboxapi.com/

It is also important to note that this is an unofficial API and the current API limit is 250 calls per hour. If you go over that amount by too great a deal that you IP will get blocked automatically for Spam and you will need to email the owner of the site to request it to be unblocked (as I found out while building this widget). If the API site goes down or is unavailable for whatever reason, this this widget will fail. I do know of another API site, but some of the features available on this widget do not line up properly with that site.

Just supply a gamertag to the script via the –Gamertag parameter. In this case, I will look at Major Nelson’s information.

.\xboxlivewidget.ps1 -Gamertag "Major Nelson"

image

You can access the context menu be right clicking anywhere on the widget and either select Exit to close the widget out or viewing Online Friends.

image

Once you select Show Online Friends, a new window opens up displaying all of the online friends. Even better is that this is an auto-updating window, so as the main window updates the number of friends, this window will update as well!

image

I am still working on a better way to refresh the online friends window to just remove the offline friends instead of clearing the window and re-building the list. That will be hopefully ready for the next release.

The basics of this is using Invoke-WebRequest (PowerShell V3 only) to query the API while providing the Gamertag in the url request, using my gamertag: El Gato Rabioso.

$tempGamertag = "El Gato Rabioso"
Add-Type System.Web
$Gamertag = [System.Web.HttpUtility]::UrlEncode($Gamertag.Trim())($tempGamertag.Trim())
[xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/profile/{0}" -f $Gamertag) | Select-Object -ExpandProperty Content)

To quickly break this down, I am first supplying a Gamertag that will be used in the request to get the information I need. But, before I can do that, I need to take care of those spaces in the name, otherwise the request will fail miserably. To handle the proper encoding I will enlist the help of [System.Web.HttpUtility] with the UrlEncode method to take those spaces and replace them with a “+” sign instead. If there were other special characters in the name, they would be properly translated as well into their respected character.

With that out of the way, I then perform the request to the API site and download the content which I cast to XML using [xml] so I now have a nice structured object to work with and select only what I need from it.

But this isn’t really about using the API from the command line, if you want more information about that, I highly suggest you check out Jonathan’s articles and play with the APIs yourself. Smile

The Code

I won’t go over each and every line of code as it is fairly large and somewhat redundant in areas. Instead, I will go over the parts of the code that I feel is interesting enough to talk about.

Param (
    $Gamertag = "Major Nelson"
)
#region Gamertag Encoding
Add-Type -AssemblyName System.Web
#Encode the gamertag so it works in the API call
$updatedGamertag = [System.Web.HttpUtility]::UrlEncode($Gamertag.Trim())
#endregion Gamertag Encoding
#region Build Synchronized Collections
$xboxLive = [hashtable]::Synchronized(@{})
$ui = [hashtable]::Synchronized(@{})
$childui = [hashtable]::Synchronized(@{})
$runspace = [hashtable]::Synchronized(@{})
#endregion Build Synchronized Collections
#region Default Settings and Items for Collections
$xboxLive.Gamertag = $updatedGamertag
$xboxLive.apiLimit = 250
$childui.onlineFriendsActiveWindow = $False
$runspace.Flag = $True
$runspace.errors = $Error
$xboxLive.isAtAPILimit = $False
#endregion Default Settings and Items for Collections
#region Main Runspace

Here I set up the parameter to accept a Gamertag and then handle the encoding to make it more http request friendly as well as setting up the synchronized collections to handle the data that is being shared between each runspace thread. I also set up some settings such as the API limit and whether the child window for Online Friends is active.

        #Internal function to update child UI  
        Function New-GridItem {
            [cmdletbinding(DefaultParameterSetName='Body')]
            Param (
                [parameter(ParameterSetName='Object')]
                $InputObject
            )

            #region Build Parent Grid
            $grid = New-Object Windows.Controls.Grid
            $grid.Width = 250
            $grid.tag = $InputObject.Gamertag
            $column1 = New-Object Windows.Controls.ColumnDefinition
            $column1.Width = "Auto"
            $column2 = New-Object Windows.Controls.ColumnDefinition
            $grid.ColumnDefinitions.Add($column1) | Out-Null
            $grid.ColumnDefinitions.Add($column2) | Out-Null
            #endregion Build Parent Grid

            #region Child Grid
            $childGrid = New-Object Windows.Controls.Grid
            $row1 = New-Object Windows.Controls.RowDefinition
            $row1.Height = "Auto"
            $row2 = New-Object Windows.Controls.RowDefinition
            $row2.Height = "Auto"
            $childGrid.RowDefinitions.Add($row1) | Out-Null
            $childGrid.RowDefinitions.Add($row2) | Out-Null
            [Windows.Controls.Grid]::SetColumn($childGrid,1)
            [Windows.Controls.Grid]::SetColumnSpan($childGrid,2)
            $grid.Children.Add($childGrid) | Out-Null
            #endregion Child Grid
    
            #region Add Controls to Child Grid
            $nameTextBlock = New-Object Windows.Controls.TextBlock
            $nameTextBlock.Text = $InputObject.Gamertag
            $nameTextBlock.FontWeight = 'Bold'  
            $nameTextBlock.FontSize = '14'  
            $nameTextBlock.Foreground = 'White'
            [Windows.Controls.Grid]::SetRow($nameTextBlock,0)

            $notesTextBlock = New-Object Windows.Controls.TextBlock
            $notesTextBlock.TextWrapping="Wrap"
            $notesTextBlock.Text = $InputObject.Presence
            $notesTextBlock.Foreground = 'White'
            [Windows.Controls.Grid]::SetRow($notesTextBlock,1)

            $childGrid.Children.Add($nameTextBlock) | Out-Null
            $childGrid.Children.Add($notesTextBlock) | Out-Null
            #endregion Add Controls to Child Grid

            #region Add Controls to Parent Grid
            $image = New-Object Windows.Controls.Image
            $image.Source = $InputObject.GamerTileurl
            $image.Width = 50
            $image.Height = 50
            [Windows.Controls.Grid]::SetRow($image,0)
            [Windows.Controls.Grid]::SetColumn($image,0)
            [Windows.Controls.Grid]::SetRowSpan($image,2)

            $grid.Children.Add($image) | Out-Null
            #endregion Add Controls to Parent Grid

            Write-Output $grid
        }    
    #Internal Function to quickly create tooltip
    Function New-ToolTip {
        Param (
            [parameter()]
            $Header,
            [parameter()]
            $Body,
            [parameter()]
            $HeaderColor = "Black"
        )
        $stackPanel = New-Object Windows.Controls.StackPanel
            If ($Header) {
                $headerText = New-Object Windows.Controls.TextBlock
                $headerText.FontWeight = 'Bold'
                $headerText.Text = $Header
                $headerText.Foreground = $HeaderColor
                $stackPanel.AddChild($headerText) | Out-Null
            }
            If ($Body) {
                $bodyText = New-Object Windows.Controls.TextBlock
                $bodyText.TextWrapping = "Wrap"
                $bodyText.Text = $Body | Out-String   
                $stackPanel.AddChild($bodyText) | Out-Null
            }
        Write-Output $stackPanel
    }

New-ToolTip and New-Grid are my helper functions that make it easier to build dynamic UIs into the Widget. The tooltip function just adds a richer tooltip for the recent activity while the grid function is used on the child window for online friends to output a grid that has a couple textblocks and image controls to a listview to make viewing online friends easier. I originally had this as another tooltip, but quickly realized the disaster that it would have been.

            #Allow API calls if an hour has passed and currently unable to make call due to limit
            If ($xboxLive.isAtAPILimit -AND ((Get-Date).AddHours(-1) -gt $initalTime)) {
                $initalTime = Get-Date
                $xboxLive.isAtAPILimit = $False
            }
            #region API Calls
            If ([int]$xboxLive.api -lt [int]$xboxLive.apiLimit -AND -Not $xboxLive.isAtAPILimit) {
                $xboxLive.xboxProfile = [xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/profile/{0}" -f $xboxLive.Gamertag) | 
                    Select-Object -ExpandProperty Content)
                $xboxLive.api = ($xboxLive.xboxProfile.data.API_Limit -split "/")[0]
            } Else {
                $xboxLive.isAtAPILimit = $True
            }
            If ([int]$xboxLive.api -lt [int]$xboxLive.apiLimit -AND -Not $xboxLive.isAtAPILimit) {
                $xboxLive.xboxGames = ([xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/games/{0}" -f $xboxLive.Gamertag)))
                $xboxGames = $xboxLive.xboxGames.Data.Games | Select -First 5
                $xboxLive.api = ($xboxLive.xboxGames.data.API_Limit -split "/")[0]
            } Else {
                $xboxLive.isAtAPILimit = $True
            }
            If ([int]$xboxLive.api -lt [int]$xboxLive.apiLimit -AND -Not $xboxLive.isAtAPILimit) {
                $xboxLive.xboxFriends = [xml](Invoke-WebRequest -URI ("https://xboxapi.com/xml/friends/{0}" -f $xboxLive.Gamertag))   
                $xboxLive.api = ($xboxLive.xboxFriends.data.API_Limit -split "/")[0]
            } Else {
                $xboxLive.isAtAPILimit = $True
            }

This is the API calls to get all of the information needed to properly update the widget. I have checks in place to make sure that I do not go over the API limit within an hour. Once an hour is up, the API limit resets itself. Of course, if you go over the API limit the widget will wait a full hour before it will allow another call. I am still working a better way to handle this other than a full hour wait, but that will be in the next version.

            #region UI Update            
            $ui.gamertagpicture.Dispatcher.Invoke("Normal",[action]{
                #region Xbox Live Profile
                If ((("{0}" -f $ui.gamertagpicture.Source) -ne $xboxLive.xboxProfile.data.Player.Avatar.Gamerpic.Large)) {
                    $ui.gamertagpicture.Source = $xboxLive.xboxProfile.data.Player.Avatar.Gamerpic.Large
                }
            })
            $ui.achievementpoints.Dispatcher.Invoke("Normal",[action]{
                $ui.achievementpoints.Text = $xboxLive.xboxProfile.Data.Player.Gamerscore
            })
            $ui.gamertag_txt.Dispatcher.Invoke("Normal",[action]{
                $ui.gamertag_txt.Text = $xboxLive.xboxProfile.Data.Player.Gamertag
                If ($xboxLive.xboxProfile.Data.Player.Status.Online) {
                    $ui.onlinestate.Text = 'Online'    
                } Else {
                    $ui.onlinestate.Text = 'Offline'
                }
            })
            $ui.status.Dispatcher.Invoke("Normal",[action]{
                $ui.status.Text = $xboxLive.xboxProfile.data.Player.Status.Online_Status
            })
                #endregion
                #region Xbox Live Friends             
                $totalFriends = @($xboxLive.xboxFriends.data.Friends)
                $xboxLive.onlineFriends = @($xboxLive.xboxFriends.data.Friends | Where {$_.IsOnline})
                $online = $onlineFriends | Select -Expand Gamertag
                $ui.friends.Dispatcher.Invoke("Normal",[action]{
                    $ui.friends.Text = ("{0} / {1} Friends Online" -f $xboxLive.onlineFriends.count,$totalFriends.count)                    

                })
                If ($xboxLive.onlineFriends.Count -gt 0 -AND $childui.onlineFriendsActiveWindow) {
                    $childui.onlineFriendsListView.Dispatcher.Invoke("Normal",[action]{
                        $childui.onlineFriendsListView.Items.Clear()                       
                    })

                    $xboxLive.onlineFriends | ForEach {
                        $xboxLive.onlineUser = $_
                        $childui.onlineFriendsListView.Dispatcher.Invoke("Normal",[action]{                              
                            $childui.onlineFriendsListView.Items.Add((New-GridItem -InputObject $xboxlive.onlineUser)) | Out-Null              
                        })
                    }   
                }
                If ($xboxLive.onlineFriends.Count -gt 0 -AND $ui.onlineFriendsActiveWindow) {
                    $childui.onlineFriendsListView.Dispatcher.Invoke("Normal",[action]{
                        $childui.onlineFriendsListView.Items.Clear()                       
                    })                
                }
                #endregion
                #region Recent Games
            $ui.game0_image.Dispatcher.Invoke("Normal",[action]{               
                If ((("{0}" -f $ui.game0_image.Source) -ne $xboxGames[0].boxart.large)) {
                    $ui.game0_image.Source = $xboxGames[0].boxart.large
                }
                $ui.ui.NavigateUri = $xboxGames[0].MarketplaceURL
                $ui.game0_link.ToolTip = New-ToolTip -Header $xboxGames[0].Name -Body @"
Achievements: $($xboxGames[0].Progress.Achievements) / $($xboxGames[0].PossibleAchievements)
Points: $($xboxGames[0].Progress.Score) / $($xboxGames[0].PossibleScore)
"@      
            })
            $ui.game1_image.Dispatcher.Invoke("Normal",[action]{ 
                If ((("{0}" -f $ui.game1_image.Source) -ne $xboxGames[1].boxart.large)) {
                    $ui.game1_image.Source = $xboxGames[1].boxart.large
                }
                $ui.game1_link.NavigateUri = $xboxGames[1].MarketplaceURL
                $ui.game1_link.ToolTip = New-ToolTip -Header $xboxGames[1].Name -Body @"
Achievements: $($xboxGames[1].Progress.Achievements) / $($xboxGames[1].PossibleAchievements)
Points: $($xboxGames[1].Progress.Score) / $($xboxGames[1].PossibleScore)
"@ 
            })
            $ui.game2_image.Dispatcher.Invoke("Normal",[action]{
                If ((("{0}" -f $ui.game2_image.Source) -ne $xboxGames[2].boxart.large)) {
                    $ui.game2_image.Source = $xboxGames[2].boxart.large
                }
                $ui.game2_link.NavigateUri = $xboxGames[2].MarketplaceURL
                $ui.game2_link.ToolTip = New-ToolTip -Header $xboxGames[2].Name -Body @"
Achievements: $($xboxGames[2].Progress.Achievements) / $($xboxGames[2].PossibleAchievements)
Points: $($xboxGames[2].Progress.Score) / $($xboxGames[2].PossibleScore)
"@ 
            })
            $ui.game3_image.Dispatcher.Invoke("Normal",[action]{
                If ((("{0}" -f $ui.game3_image.Source) -ne $xboxGames[3].boxart.large)) {
                    $ui.game3_image.Source = $xboxGames[3].boxart.large
                }
                $ui.game3_link.NavigateUri = $xboxGames[3].MarketplaceURL
                $ui.game3_link.ToolTip = New-ToolTip -Header $xboxGames[3].Name -Body @"
Achievements: $($xboxGames[3].Progress.Achievements) / $($xboxGames[3].PossibleAchievements)
Points: $($xboxGames[3].Progress.Score) / $($xboxGames[3].PossibleScore)
"@  
            })
            $ui.game4_image.Dispatcher.Invoke("Normal",[action]{
                If ((("{0}" -f $ui.game4_image.Source) -ne $xboxGames[4].boxart.large)) {
                    $ui.game4_image.Source = $xboxGames[4].boxart.large
                }
                $ui.game4_link.NavigateUri = $xboxGames[4].MarketplaceURL
                $ui.game4_link.ToolTip = New-ToolTip -Header $xboxGames[4].Name -Body @"
Achievements: $($xboxGames[4].Progress.Achievements) / $($xboxGames[4].PossibleAchievements)
Points: $($xboxGames[4].Progress.Score) / $($xboxGames[4].PossibleScore)
"@               
            })  
                #endregion Recent Games
    #endregion
        #Wait 30 seconds before making next API call
        Start-Sleep -Seconds 30
        }

This is where all of the UI updates take place. If you remember from my previous blog post, you will see some familiar techniques being used with the dispatcher object on each UI control. An important thing to note is that you should avoid performing any sort of data collection or long running commands during the use of the Invoke method of the dispatcher as it will crush your UI and cause it to freeze up. So make sure that you do all of the data gathering prior to calling the dispatcher and just add the data during that time to the proper control.

    $ui.onlinefriendsmenu.Add_Click({
        $rs = [RunspaceFactory]::CreateRunspace()
        $rs.ApartmentState = “STA”
        $rs.ThreadOptions = “ReuseThread”
        $rs.Open() | Out-Null
        $runspace.__PowerShell = {Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase}.GetPowerShell() 
        $rs.SessionStateProxy.SetVariable('xboxLive',$xboxLive)
        $rs.SessionStateProxy.SetVariable('ui',$ui)
        $rs.SessionStateProxy.SetVariable('runspace',$runspace)
        $rs.SessionStateProxy.SetVariable('childui',$childui)
        $runspace.__PowerShell.Runspace = $rs 
        $runspace.__PowerShell.AddScript({ 
                Function New-GridItem {
                    [cmdletbinding(DefaultParameterSetName='Body')]
                    Param (
                        [parameter(ParameterSetName='Object')]
                        $InputObject
                    )

                    #region Build Parent Grid
                    $grid = New-Object Windows.Controls.Grid
                    $grid.Width = 250
                    $grid.tag = $InputObject.Gamertag
                    $column1 = New-Object Windows.Controls.ColumnDefinition
                    $column1.Width = "Auto"
                    $column2 = New-Object Windows.Controls.ColumnDefinition
                    $grid.ColumnDefinitions.Add($column1) | Out-Null
                    $grid.ColumnDefinitions.Add($column2) | Out-Null
                    #endregion Build Parent Grid

                    #region Child Grid
                    $childGrid = New-Object Windows.Controls.Grid
                    $row1 = New-Object Windows.Controls.RowDefinition
                    $row1.Height = "Auto"
                    $row2 = New-Object Windows.Controls.RowDefinition
                    $row2.Height = "Auto"
                    $childGrid.RowDefinitions.Add($row1) | Out-Null
                    $childGrid.RowDefinitions.Add($row2) | Out-Null
                    [Windows.Controls.Grid]::SetColumn($childGrid,1)
                    [Windows.Controls.Grid]::SetColumnSpan($childGrid,2)
                    $grid.Children.Add($childGrid) | Out-Null
                    #endregion Child Grid
    
                    #region Add Controls to Child Grid
                    $nameTextBlock = New-Object Windows.Controls.TextBlock
                    $nameTextBlock.Text = $InputObject.Gamertag
                    $nameTextBlock.FontWeight = 'Bold'  
                    $nameTextBlock.FontSize = '14'  
                    $nameTextBlock.Foreground = 'White'
                    [Windows.Controls.Grid]::SetRow($nameTextBlock,0)

                    $notesTextBlock = New-Object Windows.Controls.TextBlock
                    $notesTextBlock.TextWrapping="Wrap"
                    $notesTextBlock.Text = $InputObject.Presence
                    $notesTextBlock.Foreground = 'White'
                    [Windows.Controls.Grid]::SetRow($notesTextBlock,1)

                    $childGrid.Children.Add($nameTextBlock) | Out-Null
                    $childGrid.Children.Add($notesTextBlock) | Out-Null
                    #endregion Add Controls to Child Grid

                    #region Add Controls to Parent Grid
                    $image = New-Object Windows.Controls.Image
                    $image.Source = $InputObject.GamerTileurl
                    $image.Width = 50
                    $image.Height = 50
                    [Windows.Controls.Grid]::SetRow($image,0)
                    [Windows.Controls.Grid]::SetColumn($image,0)
                    [Windows.Controls.Grid]::SetRowSpan($image,2)

                    $grid.Children.Add($image) | Out-Null
                    #endregion Add Controls to Parent Grid

                    Write-Output $grid
                }
                #Launch new window showing online friends
                [xml]$onlineFriendsXAML =  @"
                <Window
                    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    Title="MainWindow" SizeToContent="WidthAndHeight" ResizeMode="NoResize" WindowStyle="None" AllowsTransparency="True" 
                    Background="Transparent" Topmost="True">
                    <Grid Height="240" Width="330">
                        <Rectangle Height="240" HorizontalAlignment="Left" Name="rectangle1" VerticalAlignment="Top" Width="320" RadiusX="15" RadiusY="15">
                            <Rectangle.Fill>
                                <LinearGradientBrush StartPoint='0,0' EndPoint='0,1'>
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Color="Green" Offset='0' />
                                        <GradientStop Color="White" Offset='1' />
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Rectangle.Fill>
                        </Rectangle>
                        <Label Content="Online Friends" Height="28" HorizontalAlignment="Left" Margin="12,8,0,0" VerticalAlignment="Top" Width="276" 
                        Foreground="White" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" FontWeight="Bold" FontSize="14" >
                            <Label.Effect>
                                <DropShadowEffect Color = 'Black' ShadowDepth = '2' BlurRadius = '8' />
                            </Label.Effect>                
                        </Label>
                        <ListView Height="200" HorizontalAlignment="Left" Margin="9,36,0,0" Name="listview" VerticalAlignment="Top" Width="300">
                            <ListView.Background>
                                <SolidColorBrush />
                            </ListView.Background>
                        </ListView>
                    </Grid>
                </Window>
"@
                $onlinereader=(New-Object System.Xml.XmlNodeReader $onlineFriendsXAML)
                $childui.onlineFriendsWindow = [Windows.Markup.XamlReader]::Load($onlinereader)               
                $childui.onlineFriendsListView = $childui.onlineFriendsWindow.FindName('listview')
                $xboxLive.onlineFriends | ForEach {
                    $childui.onlineFriendsListView.Items.Add((New-GridItem -InputObject $_)) | Out-Null
                }     
                $childui.onlineFriendsWindow.Add_MouseRightButtonUp({$This.Close()})
                $childui.onlineFriendsWindow.Add_MouseLeftButtonDown({$This.DragMove()})
                $childui.onlineFriendsWindow.Add_Closed({
                    $ui.onlineFriendsActiveWindow = $False
                    $runspace.__PowerShell.EndInvoke($runspace.__Handle)
                    $runspace.__PowerShell.Dispose()
                })
                $childui.onlineFriendsActiveWindow = $True
                $childui.onlinefriendsWindow.ShowDialog() | Out-Null
        }) | Out-Null
        $runspace.__Handle = $runspace.__PowerShell.BeginInvoke()

This is notable because when you use the context menu to select to view the Online Friends, this will spin up a new runspace and then open the new window that shows all of the online friends.

Everything else in the code is the usual mix of XAML code and reading the XAML into PowerShell and connecting to the controls and managing various events.

Let me know what you think of this widget as well as anything else you would like to see from it! I have plans to add more features to this as well as squashing any bugs that I can find.

I had a great time putting this together and hope you enjoy it as well! Give it a download at the link below.

Download the Widget

Script Repository

This entry was posted in GUI, powershell, WPF and tagged , , , , , , . Bookmark the permalink.

Leave a comment