Skip to content

Instantly share code, notes, and snippets.

@evandcoleman
Last active April 13, 2023 02:45
Show Gist options
  • Save evandcoleman/a2ab0e15a691ba8e8894a38836e054fd to your computer and use it in GitHub Desktop.
Save evandcoleman/a2ab0e15a691ba8e8894a38836e054fd to your computer and use it in GitHub Desktop.
Scriptable widget for MLB scores
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: magic;
const TEAM = "NYY";
/////////////////////////////////////////
//
// Cache
//
/////////////////////////////////////////
class Cache {
constructor(name) {
this.fm = FileManager.iCloud();
this.cachePath = this.fm.joinPath(this.fm.documentsDirectory(), name);
if (!this.fm.fileExists(this.cachePath)) {
this.fm.createDirectory(this.cachePath)
}
}
async read(key, expirationMinutes) {
try {
const path = this.fm.joinPath(this.cachePath, key);
await this.fm.downloadFileFromiCloud(path);
const createdAt = this.fm.creationDate(path);
if (expirationMinutes) {
if ((new Date()) - createdAt > (expirationMinutes * 60000)) {
this.fm.remove(path);
return null;
}
}
const value = this.fm.readString(path);
try {
return JSON.parse(value);
} catch(error) {
return value;
}
} catch(error) {
return null;
}
};
write(key, value) {
const path = this.fm.joinPath(this.cachePath, key.replace('/', '-'));
console.log(`Caching to ${path}...`);
if (typeof value === 'string' || value instanceof String) {
this.fm.writeString(path, value);
} else {
this.fm.writeString(path, JSON.stringify(value));
}
}
}
///////////////////////////////////////////
const cache = new Cache("mlbWidgetCache");
const widget = await createWidget();
Script.setWidget(widget);
Script.complete();
async function createWidget() {
const w = new ListWidget()
w.backgroundColor = new Color("#0F1011");
w.setPadding(15, 10, 15, 15)
const mainStack = w.addStack();
mainStack.layoutVertically();
const { game, team } = await fetchTeam(TEAM);
const awayLogo = await fetchTeamLogo(game.teams.away.team.abbreviation);
const homeLogo = await fetchTeamLogo(game.teams.home.team.abbreviation);
const scoreStack = mainStack.addStack();
scoreStack.layoutVertically();
const awayStack = scoreStack.addStack();
awayStack.centerAlignContent();
const awayLogoImage = awayStack.addImage(awayLogo);
awayLogoImage.imageSize = new Size(42, 42);
awayStack.addSpacer();
const awayName = awayStack.addText(game.teams.away.team.abbreviation);
awayName.font = Font.title2();
awayStack.addSpacer();
const awayRuns = awayStack.addText(`${game.linescore.teams.away.runs || ''}`);
awayRuns.font = Font.title2();
const spacer = scoreStack.addSpacer();
spacer.length = 6;
const homeStack = scoreStack.addStack();
homeStack.centerAlignContent();
const homeLogoImage = homeStack.addImage(homeLogo);
homeLogoImage.imageSize = new Size(42, 42);
homeStack.addSpacer();
const homeName = homeStack.addText(game.teams.home.team.abbreviation);
homeName.font = Font.title2();
homeStack.addSpacer();
const homeRuns = homeStack.addText(`${game.linescore.teams.home.runs || ''}`);
homeRuns.font = Font.title2();
mainStack.addSpacer();
const statusStack = mainStack.addStack();
statusStack.layoutHorizontally();
statusStack.addSpacer();
const statusText = statusStack.addText(getFormattedStatus(game));
statusText.font = Font.callout();
statusText.textColor = Color.lightGray();
return w
}
function getFormattedStatus(game) {
const status = game.status.abstractGameState;
const innings = game.linescore.innings.length;
switch (status) {
case "Final":
case "Completed Early":
case "Game Over":
if (innings !== 9) {
return `${status}/${innings}`;
} else {
return status;
}
case "Delayed":
return `${status}/${innings}`;
case "Suspended":
return `${status}/${innings}`;
case "In Progress":
return `${game.linescore.inningState} ${game.linescore.currentInning}`;
case "Preview":
case "Pre-Game":
const df = new DateFormatter();
df.useNoDateStyle();
df.useShortTimeStyle();
return df.string(new Date(game.gameDate));
default:
return status;
}
}
async function fetchTeam(team) {
const scoreboard = await fetchScoreboard();
const games = scoreboard.filter(game => {
const away = game.teams.away.team.abbreviation;
const home = game.teams.home.team.abbreviation;
return team === away || team === home;
});
const game = games[0];
const isHome = game.teams.home.team.abbreviation === team;
return {
game,
team: isHome ? game.teams.home.team : game.teams.away.team,
};
}
async function fetchScoreboard() {
const df = new DateFormatter();
df.dateFormat = "yyyy-MM-dd";
const now = new Date();
const date = now.getHours() < 5 ? new Date(now.getTime() - 43200000) : now;
const dateString = df.string(date);
const url = `https://statsapi.mlb.com/api/v1/schedule?date=${dateString}&language=en&hydrate=team(league),venue(location,timezone),linescore(matchup,runners,positions),decisions,homeRuns,probablePitcher,flags,review,seriesStatus,person,stats,broadcasts(all)&sportId=1`;
const data = await fetchJson(`mlb_scores_${dateString}`, url);
return data.dates[0].games;
}
async function fetchTeamLogo(team) {
const req = new Request(`https://a.espncdn.com/i/teamlogos/mlb/500/${team.toLowerCase()}.png`);
return req.loadImage();
}
async function fetchJson(key, url, headers) {
const cached = await cache.read(key, 5);
if (cached) {
return cached;
}
try {
console.log(`Fetching url: ${url}`);
const req = new Request(url);
req.headers = headers;
const resp = await req.loadJSON();
cache.write(key, resp);
return resp;
} catch (error) {
try {
return cache.read(key, 5);
} catch (error) {
console.log(`Couldn't fetch ${url}`);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment