-
Notifications
You must be signed in to change notification settings - Fork 1
Creating an XBOX Live Gamerscore Custom App
Using the new Config Script
feature, you can execute Node.js methods to dynamically build the configuration values passed to the Render Script
. This allows you to tap into the vast ecosystem of modules available on npm to build your own connected applets.
In this tutorial, we will walk through the steps required to create your own Gamerscore applet that updates automatically on your desired schedule.
-
Pixlet installed in your
$PATH
- Node.js 12+
- Homebridge 1.3.0+
- homebridge-tidbyt installed and configured with your API key and device ID
Our new custom app will consist of two different parts:
-
A Starlark render script that is used to create a webp animation using
pixlet render
. -
A Node.js config script that will use the XBOX Live API to get profile.
We'll need to create some files that are accessible to the homebridge-tidbyt plugin and Homebridge as a whole.
I'm running Homebridge on a pi, so I'm going to be using /home/pi/tidbyt/xbox-live
as my path for purpose of this article. Please replace this with a path that makes sense for your setup.
Create a new directory to house the custom app under your projects root:
> mkdir -p /home/pi/tidbyt/xbox-live
After several iterations, I've settled on the following minimal design:
I thought it might be nice to have the XBOX avatar displayed as well, here is a version with that:
Here is my finished render.star
Starlark script:
# Tidbyt Xbox Live App
# MIT License
# by Nicholas Penree, Dec 29 2021
load("render.star", "render")
load("http.star", "http")
load("encoding/base64.star", "base64")
load("cache.star", "cache")
MOCK=False
def get_config(config):
if MOCK:
return dict(
gamer_tag = "z3r0c00l",
gamer_score = "10808",
)
return config
def GamerScoreLogo():
return render.Circle(
color = "#fff",
diameter = 13,
child = render.Text(
content = "G",
color = "#000",
font = "Dina_r400-6"
),
)
def GamerScore(gamer_score):
return render.Row(
expanded=True,
main_align="center",
cross_align="center",
children = [
GamerScoreLogo(),
render.Box(width = 1),
render.Text(content = gamer_score),
],
)
def main(config):
settings = get_config(config)
gamer_tag = settings.get('gamer_tag', '')
gamer_score = settings.get('gamer_score', '')
avatar_url = settings.get('avatar_url', '')
children = []
if avatar_url != '':
children += [
render.Box(height = 2),
render.Image(src=http.get(avatar_url).body(), height=14, width=14)
]
children += [ GamerScore(gamer_score) ]
if gamer_tag != '':
if settings.get('scroll', False) or len(gamer_tag) > 8:
gamer_tag_child = render.Marquee(
width = 64,
child = render.Text(color = '#3c3c3c', content = gamer_tag),
)
else:
gamer_tag_child = render.Text(color = '#3c3c3c', content = gamer_tag)
children += [ gamer_tag_child ]
return render.Root(
child = render.Box(
render.Column(
expanded=True,
main_align="center",
cross_align="center",
children = children,
),
),
)
Download it and move it into your project folder, with the name render.star
(ie./home/pi/tidbyt/xbox-live/render.star
).
-
gamer_score
- Required. The gamerscore value to show. -
gamer_tag
- Optional. The gamertag of the profile. -
avatar_url
- Optional. Profile avatar image URL that will be displayed instead of the Gamescore icon.
These values will need to be passed by our Config Script
. We'll do that next!
One of the great things about Config Scripts is that they are written in JavaScript using Node.js modules. This allows us to avoid reventing the wheel when someone else has already written code that does what we want.
We'll leverage two npm modules to deal with communicating with the XBOX Live API: @xboxreplay/xboxlive-auth
and @xboxreplay/xboxlive-api
.
Let's create an npm project and install these two packages as dependencies:
> cd /home/pi/tidbyt/xbox-live && npm init -y
> npm install @xboxreplay/xboxlive-auth @xboxreplay/xboxlive-api
This will create a package.json file and install these two packages in the node_modules
folder under your project directory.
A Config Script
is a very simple Node module. It exports a single function that returns a Promise
that resolves to an array of key/value pair objects.
Here is a quick implementation that uses our new dependencies to fetch and map your XBOX profile data to the configuration parameters we created in the render script:
/*!
* Tidbyt Xbox Live App
* MIT License
* by Nicholas Penree, Dec 29 2021
*/
const { authenticateWithUserRefreshToken } = require('@xboxreplay/xboxlive-auth')
const XboxLiveAPI = require('@xboxreplay/xboxlive-api')
async function fetch(config = []) {
let auth
let playerSettings
let { value: refreshToken } = (config || []).find(c => c.key === 'refresh_token') || {}
let { value: clientId } = (config || []).find(c => c.key === 'client_id') || {}
if (!clientId) {
// Defaults to the live (live.com) client ID used by the official Xbox desktop / mobile
// applications during the authentication (OAuth) process.
// Each live application has its own ID but this one allows service::user.auth.xboxlive.com::MBI_SSL scope
// usage which skip the authorization prompt and return user's token.
//
// source: https://github.com/XboxReplay/xboxlive-auth/issues/16#issuecomment-815601103
//
clientId = process.env.XBOX_LIVE_CLIENT_ID || '0000000048093EE3'
}
if (!refreshToken) {
refreshToken = process.env.XBOX_LIVE_REFRESH_TOKEN
}
try {
auth = await authenticateWithUserRefreshToken(refreshToken, { clientId })
} catch (err) {
console.error(err.details);
}
const {
user_hash: userHash,
xuid,
xsts_token: XSTSToken
} = auth || {}
if (xuid) {
try {
playerSettings = await XboxLiveAPI.getPlayerSettings(xuid, {
userHash,
XSTSToken,
}, [
'UniqueModernGamertag',
// 'GameDisplayPicRaw',
'Gamerscore',
])
} catch (error) {
console.error(error)
}
}
playerSettings = playerSettings || []
const { value: gamerTag } = playerSettings.find(x => x.id === 'UniqueModernGamertag') || {}
const { value: gamerScore } = playerSettings.find(x => x.id === 'Gamerscore') || {}
// const { value: avatarImageUrl } = playerSettings.find(x => x.id === 'GameDisplayPicRaw') || {}
return [
// ...config,
{ key: 'gamer_tag', value: gamerTag },
{ key: 'gamer_score', value: gamerScore },
// { key: 'avatar_url', value: avatarImageUrl },
]
}
module.exports = fetch;
Download it and move it into your project folder, with the name config.js
(ie./home/pi/tidbyt/xbox-live/config.js
).
-
Login to Xbox Live (using this link)
-
Once logged in, you will see a white screen. Find the
refresh_token
value from the URL. You'll want to extract everything afterrefresh_token=
and before the next&
. -
To use the refresh token, you will need to URI decode it. You can do this using the Node.js CLI like this:
> node -e 'console.log("Decoded refresh_token:\n\n" + decodeURIComponent(`<your refresh token>`))'
Make sure to replace <your refresh token>
with the actual refresh token value you extracted from the URL bar.
You should have a value printed to your console. Copy/take note of this value, you'll need it to configure the custom app definition in the next step.
Now that we have generated our new refresh token, we can configure the custom app in our Homebridge configuration.
Find your Tidbyt platform object and add to the customApps
array like this:
{
"platform": "Tydbit",
"managedDevices": [
...
],
"customApps": [
...,
{
"id": "XboxGamerScore",
"enabled": true,
"updateOnStartup": true,
"script": "/home/pi/tidbyt/xbox-live/render.star",
"schedule": "*/30 * * * *",
"configScript": "/home/pi/tidbyt/xbox-live/config.js",
"config": [
{
"key": "refresh_token",
"value": "<your refresh token>"
}
]
}
]
}
Make sure to replace <your refresh token>
with the actual decoded refresh token you obtained above. If you omit the refresh_token
config value, the code will fallback to using the XBOX_LIVE_REFRESH_TOKEN
environment variable.
Now that we've sucessfully configured the new applet, we can enable it by restarting Homebridge.
After you restart, you should see [homebridge-tidbyt] executing your new XboxGamerScore
applet and pushing your sweet new Gamerscore to your Tidbyt.
Here is a shot of mine:
Happy Hacking :)