02 May 2009

Baseball Better - Yahoo Greasemonkey Script

I love baseball. But you gotta admit that it can be boring "watching" a game on the web. If not watching it in person or on tv, it's kinda slow. I'm sorta ADD when cruising the web. I usually have 6 or more Firefox tabs open at once. I'm reading something, writing something, searching for something all at the same time. It's a wonder that I get anything done at all, but amazingly it works out.

So if a good game is on at the same time that I'm on-line, I like to have a tab tuned into the Yahoo Sport MLB game. But the problem is that I have to keep clicking to that tab to see what's going on. I have Greasemonkey installed and have written a few other scripts to customize my viewing pleasure so hacking the box score page should be pretty easy.

Here's what the regular game looks like. Usually there's an advertisement over on the right side above the "series at glance" table. I guess times are tough at Yahoo and advertising is slow because no ad is being displayed on this screen shot.

Notice a few other things. The Firefox tab says "MLB - Kansas City Royals/M...". I'd like this to display something useful like the current in-progress game stats. Also see the Inning Summary table at the bottom. Below that is a Scoring Summary that can't be seen unless you scroll down.

So my typical game "watching" experience is every 5-10 minutes, click the tab, look at the score. If different than last time I looked, grab the scroll bar and go way down to see what happened. Way too much work.

After some quick Greasemonkey hacking, I've come up with a better experience. As the page loads, I grab the team names, current score, inning, ball-strike count, runners on-base, and last play. Sounds like a lot but really it's not. Yahoo has everything in nice tables and getting it is like butter. Not really but I just felt like using that term. Just like buddah.

I take all those stats and format them into the tab so I can see the game info without having to click the tab. This alone has probably saved years of wear and tear on my mouse button.

I then replaced the advertisement block (yahoo isn't currently using it anyway) with the scoring summary so no painful scrolling occurs.

For those that also want to enjoy baseball a little better, I've uploaded the script to Greasemonkey script haven.

For those that don't trust me and want to see what I've done, here's a view in the sausage factory. It ain't pretty but it works.

It's just basically screen scraping by searching for classes and then parsing data. Every time Yahoo makes a tweak, it breaks the script. I don't mind, it's usually minor.

Here we go.

Yahoo sets the page to a fixed width of 974 pixels. I run in 1280 mode so this wastes lots of valuable screen space and causes unsightly white gutters. This must be changed:
function changeWidth() {
var node = document.evaluate('//table[@width="974"]',
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null);

node.snapshotItem(0).width="100%";
}
Here's a big part of the magic. Find the table that wraps the team name and score and parse it out. The team names are long and we need shorten them so they fit it the tab nicely.
function getScore() {
var scoreNode = document.evaluate(
'//td[@class="yspsctnhdln"]',
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null);
scoreNode = (scoreNode.snapshotItem(0));

// this is ugly but I want to shorten the team so all the info displays nice on the tab
var str = scoreNode.textContent;
str = str.replace(/\n/g, "");
str = str.replace(/ /g, "");
str = str.replace(/,/g, "");
str = str.replace("Arizona", "AZ");
str = str.replace("Atlanta", "Atl");
str = str.replace("Baltimore", "Bal");
str = str.replace("Boston", "Bos");
str = str.replace("Chi Cubs", "Cub");
str = str.replace("Chi White Sox", "CWS");
str = str.replace("Cincinnati", "Cin");
str = str.replace("Cleveland", "Cle");
str = str.replace("Colorado", "Col");
str = str.replace("Detroit", "Det");
str = str.replace("Florida", "FL");
str = str.replace("Houston", "Hou");
str = str.replace("Kansas City", "KC");
str = str.replace("Minnesota", "Min");
str = str.replace("Milwaukee", "Mil");
str = str.replace("LA Angels", "LAA");
str = str.replace("LA Dodgers", "LAD");
str = str.replace("NY Mets", "Met");
str = str.replace("NY Yankees", "NYY");
str = str.replace("Oakland", "Oak");
str = str.replace("Philadelphia", "Phi");
str = str.replace("Pittsburgh", "Pit");
str = str.replace("San Diego", "SD");
str = str.replace("San Francisco", "SF");
str = str.replace("Seattle", "Sea");
str = str.replace("St. Louis", "SL");
str = str.replace("Tampa Bay", "Tam");
str = str.replace("Toronto", "Tor");
str = str.replace("Texas", "Tex");
str = str.replace("Washington", "Was");
str = str.replace(/ /g, "");

// get inning and shorten
var inning = getInning();
inning = inning.replace(/\n/g, "");
inning = inning.replace(/ /g, "");
inning = inning.replace(/Bot /g, "B");
inning = inning.replace(/Top /g, "T");
inning = inning.replace(/End /g, "E");

document.title = str + ' ' + inning + ' ' + getOut();
}
In the above code, I didn't put a comment on this line but this is where the Firefox tab gets the in-game stats:
document.title = str + ' ' + inning + ' ' + getOut();
And more parsing. And then some really ugly if-else code. Kinda embarrassing that I did this. But I did and I'm too lazy to re-write.
function getOut() {
var balls = document.evaluate("//*[contains(.,'O:')]/b",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

var count = "";
if (balls) {
var n= balls.nextSibling;
var strikes = n.nextSibling;
n = strikes.nextSibling;
var outs= n.nextSibling;
count = balls.innerHTML + '-' + strikes.innerHTML + ' O:'+ outs.innerHTML;
}

// find how many men are on base.
// do this by looking at what image is being displayed.
// yeah, I need to clean this up but it works and I don't have time. go for it
var men = document.evaluate("//img[contains(@src,'tr_empty.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (men) {
men = "empty";
}
else {
men = document.evaluate("//img[contains(@src,'tr_1b.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

if (men) {
men = "1b";
}
else {
men = document.evaluate("//img[contains(@src,'tr_2b.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (men) {
men = "2b";
}
else {
men = document.evaluate("//img[contains(@src,'tr_3b.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (men) {
men = "3b";
}
else {
men = document.evaluate("//img[contains(@src,'tr_1b2b.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (men) {
men = "1b2b";
}
else {
men = document.evaluate("//img[contains(@src,'tr_1b3b.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (men) {
men = "1b3b";
}
else {
men = document.evaluate("//img[contains(@src,'tr_2b3b.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (men) {
men = "2b3b";
}
else {
men = document.evaluate("//img[contains(@src,'tr_full.gif')]",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (men) {
men = "full";
}
else {
men = "";
}
}
}
}
}
}
}
}
return count + ' ' + men;
}
And that's about it. Take a look at the file at userscripts.org and give it a try. Let me know if you've improved and found some bugs.

What do you think? Leave a comment.

1 comment: