PhantomJS Xbox Achievements

I originally tracked my xbox achievements by using macros and batch scripts, this allowed me to grab my own achievement data, and store it in my db for viewing on the web. This worked great apart from the inherent problem of have macros physically opening and closing browsers every 15 minutes of my virtual machine, every day or so the scripts would break or Firefox would freeze, so I was always having to keep on top of it, not ideal.

A few months later I came across XboxAPI.com, this handy (FREE) site wraps up all the xbox.com functionality and provides users with a simple to use api to get game info, friends, achievements and other data. So I scrapped my macros and jumped on this api, this worked spot on and still does. The only downside is that due to constraints on the Xbox.com website the achievement images were only available in black and white and this didn’t look to great on my website.

So my next plan was to use PhantomJS, after a few weeks of playing around with it on and off I didn’t get too far, then I came across this script on github, after a quick test, it did indeed log me into Xbox.com and grab my friends page. So a few hours later and some tweaks I’ve got it to get the users latest game achievement data in JSON, its not polished and could break at any time if Microsoft change the Xbox.com website, so bear that in mind. But hopefully it’ll give someone a good place to jump off from. :)

/*
This script provides a quick and dirty way of getting your most recent game achievement json data.

This is a lightly modified version of Jabbslad's script found here: https://gist.github.com/1653745

James Coverdale - imjam.es 2012

*/

var fs = require('fs'),
    system = require('system');

var name = "";

if (phantom.args.length !== 2) {
    console.log('Usage xbox.js <username> <password>');
    phantom.exit();
}

var page = new WebPage({
    'page.settings.loadImages': false
});

/*
* Callback to process page when finshed loading
*/
page.onLoadFinished = function(status) {
    if (status !== "success") {
       console.log("Unable to access network");
    } else {
        var url = getPageUrl();

        switch (true) {
        case /login.srf/.test(url):
            // Step 1 - Login
            page.evaluate(fillFormFunctionAsString());
            break;
        case /post.srf/.test(url):
            // Step 2 - Process Cookies
            break;
	case /Details/.test(url):
	    // We got the achievements
            parseGame();
            phantom.exit();
            break;
        case /Activity$/.test(url):
            // We did it!
            parseFriends();
            break;
        default:
            // uh oh we hit an unexpected url
            phantom.exit(1);
        }
    }
}

function getPageUrl() {
    return page.evaluate(function() {
        return location.href
    });
}

//This whole function will need reworking ideally, must be a better way of grabbing the json
//but this works for now
function parseGame() {

	var index = page.content.indexOf("broker.publish(routes.activity.details.load, ") + 45;
	var lastindex = page.content.indexOf("</script>", index);

	var json = page.content.substring(index, lastindex);
	json = json.split("});").join("");
	json = json + "}";

	// You could just push the json to the console to allow use in the command line, i.e. pipe it to another script.
	f = null;
	f = fs.open(name + '.json', "w");
	f.write(json);
	f.close();

}

function parseFriends() {

   try {

	   var contents = page.content.substring(page.content.indexOf("<div class=\"activityPage activity\">"), page.content.indexOf("</ol>  <div class=\"clearfix\"></div></div>"));

	   // Again this string manip is not ideal, too much that could go wrong if the site changes
	   var index = contents.indexOf("<a href=\"/en-US/Activity/Details?titleId=") + 41;
	   var url = contents.substring(index, contents.indexOf("\">", index));
	   var nameindex = contents.indexOf("alt=\"", index) + 5;
	   name = contents.substring(nameindex, contents.indexOf("\"", nameindex));

	   // make the game name lower case and remove any spaces
	   name = name.toLowerCase().split(" ").join("");

	   // Load the latest game page
	   page.open(encodeURI('https://live.xbox.com/en-US/Activity/Details?titleId=' + url));
    }
    catch (e) {
       console.log(e);
    }
}

/*
* Hack due to phantomjs page.evaluate limitations:-
* http://code.google.com/p/phantomjs/issues/detail?id=132
*/
function fillFormFunctionAsString() {
    return "function() {"
         + "var form = document.querySelector(\"form[name='f1']\"); "
         + "var login = form.querySelector(\"input[name='login']\"); "
         + "login.value = '" + phantom.args[0] + "'; "
         + "var passwd = form.querySelector(\"input[name='passwd']\"); "
         + "passwd.value = '" + phantom.args[1] + "'; "
         + "var kmsi = form.querySelector(\"input[name='KMSI']\"); "
         + "kmsi.value = '2'; "
         + "form.querySelector('#idSIButton9').click();}";
};

page.open(encodeURI('https://live.xbox.com/en-US/Activity'));

The script logs in, gets the game ID of the last game played (or currently being played), then heads to that game page for the user and pulls the JSON from the page and saves it as a .json file. You could modify it to not save the JSON and just output it to the console if you want, but in my setup I needed the .json file to upload to my webserver in order to do extra processing upon it.

To run the script simple head to PhantomJS.org and download the relevant version, runs on Windows, Mac and Linux. then simple from the command line call…

phantomjs XboxScript.js username password

Simple, you will need to edit the name of the .js file to what you have saved it, and will probably need to point to the phantomjs executable on your system.

Leave a Reply

Your email address will not be published. Required fields are marked *

*