A LibreOffice Calc add-in (UNO component, Java, MIT licensed) exposing NBA teams, games, standings, player search, and player/game statistics from the balldontlie API as worksheet functions.
New here? docs/TUTORIAL.md is a step-by-step walkthrough — install, configure your API key, and build a working one-team dashboard (PDF version). This README is the function reference.
| Function | Signature | Returns |
|---|---|---|
NBA_TEAMID |
NBA_TEAMID(name_or_abbrev; [api_key]) |
numeric team id |
NBA_TEAMS |
NBA_TEAMS([api_key]) |
spillable array: id, abbreviation, city, conference, division, full_name, name |
NBA_PLAYERSEARCH |
NBA_PLAYERSEARCH(name; [api_key]) |
spillable array: id, first_name, last_name, position, team (up to 100 matches) |
NBA_PLAYERID |
NBA_PLAYERID(full_name; [api_key]) |
numeric player id (best match) |
NBA_GAMES |
NBA_GAMES(date_or_season; [team_id]; [api_key]) |
spillable array: date, home_team, home_score, away_team, away_score, status |
NBA_SCORE |
NBA_SCORE(season; abbrev; [nth]; [api_key]) |
result string, e.g. "2024-04-10 LAL 112 - 108 BOS (W)" |
NBA_STANDINGS |
NBA_STANDINGS(season; [conference]; [api_key]) |
spillable array: conference, division, team, wins, losses, win_pct, conference_rank |
NBA_TEAMRANK |
NBA_TEAMRANK(season; abbrev; [api_key]) |
numeric conference rank |
NBA_PLAYERSTAT |
NBA_PLAYERSTAT(season; id; stat_key; [api_key]) |
season-average stat value |
NBA_BOXSCORE |
NBA_BOXSCORE(game_id; [api_key]) |
spillable array: player, team, min, pts, reb, ast, stl, blk, fg_pct, fg3_pct, ft_pct |
NBA_LASTERROR |
NBA_LASTERROR() |
most recent fetch error message (diagnostics) |
NBA_CACHECLEAR |
NBA_CACHECLEAR() |
clears the cache; returns the count cleared |
In Calc's UI, arguments are separated by semicolons:
=NBA_SCORE("2024"; "LAL").Every data function takes a trailing optional
api_key— supply it to override the environment for that one call, typed literally or (recommended, so the key isn't visible in every formula) as a cell reference:=NBA_SCORE("2024"; "LAL"; ; $B$1).
Every function above resolves against a shared cache and returns immediately — none of them ever block on network I/O or raise an exception. Instead, a cell may show one of:
| Value | Meaning |
|---|---|
#LOADING |
First request for this data. A background fetch just started. Recalculate (F9 or Ctrl+Shift+F9) once it completes — usually a second or two. |
#NO_API_KEY |
No balldontlie API key could be resolved (see below). |
#NOT_FOUND |
The request reached the API, but nothing matched (unknown team/abbreviation, empty player search, team not in that season's standings, etc). |
#ERR |
The fetch failed persistently (network error, rate limit exhausted, bad response). Call NBA_LASTERROR() for the detail message. An errored key is retried automatically ~15s later, so recalculating again often clears it. |
Responses are cached with a TTL appropriate to how often the underlying data changes, and refreshed silently in the background once stale (you keep seeing the last-known-good value while the refresh runs):
| Data | TTL |
|---|---|
| Teams | 24 hours |
| Standings | 1 hour |
| Games / scores / box scores | 5 minutes |
| Player search | 6 hours |
The cache is a bounded, thread-safe (ConcurrentHashMap-backed) in-memory
store (up to 1000 entries, oldest evicted first) that lives for the life of
the LibreOffice session — call NBA_CACHECLEAR() to force fresh data, or
restart LibreOffice.
balldontlie has required a free API key on every request since July 2026. Get one at https://www.balldontlie.io/. The key is resolved, in this priority order:
- The
api_keyfunction argument — the optional trailing argument of every data function. Type it literally, or (recommended, so the key isn't spelled out in every formula) reference a cell:=NBA_TEAMID("Lakers"; $B$1). Wins when supplied, overriding everything below for that one call. - Java system property
balldontlie.apiKey— pass-Dballdontlie.apiKey=...when launchingsoffice. - Environment variable
BALLDONTLIE_API_KEY— set it, then launchsofficefrom that same shell (or set it persistently and restart LibreOffice). - Properties file at
~/.config/libreoffice-nba/balldontlie.properties:This is the most convenient option since it doesn't depend on how LibreOffice was launched.apiKey=your_key_here
If none of the four resolve, every data function returns #NO_API_KEY
instead of failing silently or throwing. Different keys are cached
independently (the cache key includes a fingerprint of the resolved key), so
switching keys — e.g. via the argument — doesn't reuse another key's cached
data or cached error state.
# Linux/macOS, environment variable route:
export BALLDONTLIE_API_KEY='your_key'
"$LO_HOME/program/soffice"
# or the properties-file route (works regardless of launch method):
mkdir -p ~/.config/libreoffice-nba
echo 'apiKey=your_key' > ~/.config/libreoffice-nba/balldontlie.propertiesThe free tier is tight — measured live at 5 requests/minute
(x-ratelimit-limit: 5). The add-in throttles its own outgoing requests to
match (minimum ~13s apart, across a 2-thread background pool) and retries
HTTP 429 / 5xx responses with bounded exponential backoff, honoring the
numeric Retry-After header balldontlie sends on a 429, before giving up
and surfacing #ERR. Combined with the TTL cache, normal spreadsheet use
should rarely hit the limit — but expect a fresh, uncached formula to take
up to ~13s to resolve out of #LOADING if you're issuing several different
requests in a row.
Download NBA.oxt from the latest release
(currently v1.0.0,
direct link)
and install it — no build required:
mkdir -p ~/.config/libreoffice-nba
echo 'apiKey=your_key' > ~/.config/libreoffice-nba/balldontlie.properties # never hardcoded
"$LO_HOME/program/unopkg" add NBA.oxtSee "Provide the balldontlie API key" above for the other resolution mechanisms. Skip to Try it below.
export JAVA_HOME=~/jdks/jdk8u<version> # any JDK 8+; see docs/INSTALL.md
export LO_HOME=~/libreoffice26.2 # LibreOffice + SDK
./build.sh
# or pass paths explicitly:
./build.sh --jdk ~/jdks/jdk8u<version> --libreoffice ~/libreoffice26.2This runs unoidl-write → javamaker → javac --release 8 → jar → zip,
producing build/NBA.oxt. See docs/INSTALL.md for
full prerequisites (JDK 8, LibreOffice + SDK, the Java-vendor allow-list fix)
and platform-specific build/install steps (Slackware, Debian, Ubuntu,
Windows).
"$LO_HOME/program/unopkg" add --force build/NBA.oxtOr double-click build/NBA.oxt to open the Extension Manager. Restart
LibreOffice afterwards (from a shell with the API key set, if you're using
the environment-variable route).
=NBA_TEAMID("Lakers") -> 14 (example)
=NBA_TEAMS() -> spills id/abbreviation/city/... rows (array formula)
=NBA_PLAYERID("LeBron James") -> numeric player id
=NBA_PLAYERSEARCH("James") -> spills matching players (array formula)
=NBA_GAMES("2024-01-15") -> spills that day's games (array formula)
=NBA_SCORE("2024"; "LAL") -> "2024-04-10 LAL 112 - 108 BOS (W)"
=NBA_STANDINGS("2024"; "West") -> spills Western Conference standings (array formula)
=NBA_TEAMRANK("2024"; "LAL") -> numeric conference rank
=NBA_PLAYERSTAT("2024"; 237; "pts") -> season-average points
=NBA_BOXSCORE(15908) -> spills that game's per-player box score (array formula)
=NBA_LASTERROR() -> "" (or the last failure's detail)
=NBA_CACHECLEAR() -> number of entries cleared
=NBA_TEAMID("Lakers"; $B$1) -> key taken from cell B1 instead of the environment
A ready-made example workbook is at
test/nba_demo.ods — every function above, built around
the Boston Celtics (2023-24 championship season). Live results are baked in
from a real run (e.g. NBA_SCORE("2023"; "BOS") →
"2024-06-17 BOS 106 - 88 DAL (W)", the Finals-clinching game), but since the
cache is per-session, reopening it will show #LOADING again until you
configure your own API key and recalculate (Ctrl+Shift+F9, a couple of times
since fetches are async). Regenerate it with tools/build_demo.py against a
headless LibreOffice instance.
- Multi-cell / spilling. LibreOffice has no dynamic spill: to see every row of a table-returning function, select the output range and enter it as an array formula (Ctrl+Shift+Enter, or tick Array in the Function Wizard). A plain single-cell entry shows only the top-left value.
NBA_GAMESteam filter. The optionalteam_idargument takes a numeric team id (fromNBA_TEAMID), not an abbreviation — this avoids chaining two separate async lookups inside one cell call. It's honored in season mode; in date mode games are filtered client-side after the date query.NBA_SCORE/NBA_TEAMRANKeach resolve the team id from the cached team list first. If the team list hasn't loaded yet, you'll see#LOADINGonce for that (it's cached 24h afterwards, so this is rare).NBA_PLAYERID/NBA_PLAYERSEARCHname matching. balldontlie'ssearchparam substring-matches a single name field, so a natural"LeBron James"query matches nothing server-side. The add-in sends only the last word of a multi-word query to the API (usually the most distinguishing token), then ranks results client-side, preferring players whose full name contains every word you typed. Single-word queries are sent as-is.NBA_PLAYERSTATaverages the requestedstat_keyacross the player's games that season (simple mean — percentage fields likefg_pctare not attempt-weighted).min(minutes) is parsed from either a plain number or an"MM:SS"string.- No third-party jars. HTTP uses
java.net.HttpURLConnection; JSON is parsed by a small hand-rolled, tolerant parser (Json.java). Nothing beyond the JDK + UNO is bundled — avoids classloader conflicts inside the LibreOffice-embedded JVM. CompatibilityNameis set for every function inCalcAddIns.xcu, so formulas survive a save-as/reopen round trip through XLS/XLSX.- Field-name tolerance. balldontlie's exact JSON field names for
standings (win percentage, conference rank) vary slightly across API
versions; the add-in tries several common field-name spellings before
giving up. If your account's standings response uses a different field
name than those tried,
NBA_STANDINGS/NBA_TEAMRANKmay show a blank rank — please file an issue with the field name your response actually uses. - Paid-tier endpoints. Confirmed live: on a free-tier key,
/standingsand/statsboth return HTTP 401, soNBA_STANDINGS,NBA_TEAMRANK,NBA_PLAYERSTAT, andNBA_BOXSCOREwill show#ERR(withNBA_LASTERROR()reporting"...HTTP 401: Unauthorized") until you upgrade to a balldontlie plan that includes those endpoints.NBA_TEAMID,NBA_TEAMS,NBA_PLAYERID,NBA_PLAYERSEARCH,NBA_GAMES, andNBA_SCOREall work on the free tier.
See CHANGELOG.md for the release history.
Released under the MIT License.
