Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

stream-metrics

A local social media metrics aggregator for streamers. One screen, every platform, and a history the platforms do not keep for you.

Runs entirely on your own machine. No account, no subscription, no data leaving the building.

Status: 0.1, early. Twitch works end to end — login, live view, current totals, daily history, per-stream statistics, monthly aggregates, and merging broadcasts that a dropped connection split apart. Charts, automatic backups and every other platform do not exist yet. Built in the open; expect the shape of things to keep moving.


Why

Creator metrics live on six or seven different dashboards. Keeping up with them means opening six or seven tabs, every day. Tools that unify this exist and are expensive.

There is a second problem those tools charge for, and it is the more interesting one: the platforms show you today, not last month. Twitch's own dashboard is genuinely good, but its window is fixed, its format is its own, and there is no way to put Twitch and YouTube on the same axis. Some numbers — average viewers, peak viewers, how long a broadcast actually ran — are not in the API at all. They exist only if something writes them down, minute by minute.

That is what this does.

The one rule

No number reaches the screen without saying how fresh it is.

A dashboard that shows an old number without saying so is worse than no dashboard, because it produces confident wrong decisions. So every value in this application travels inside an envelope:

{ "value": 1234, "fetchedAt": "2026-08-27T14:32:11Z", "status": "OK" }

status is OK, STALE or ERROR, and the UI renders all three differently — a stale number is dimmed and timestamped, an error shows no number at all.

The rule is enforced by the type, not by discipline: ApiResponse.error() has no overload that accepts a value, so an error is structurally incapable of carrying a stale figure onto the screen. A test guards it.

The same principle runs through the storage layer. A day with no data has no row — never an interpolated one. Every collection attempt is logged, including the failures, so a gap in a chart can be explained rather than merely noticed.

And it runs through the parser, which is where it was nearly lost: a JSON library will happily read a missing number as zero. A reply with no follower count was being stored as "0 followers" and shown as trustworthy. Every field is now required, and 0 and "no answer" are different facts with a test that keeps them apart.

Where the numbers come from

Not every figure on the screen carries the same weight, so the interface says which is which.

Source Shown as
Followers, subscribers, growth Twitch's own values exact
Streams, hours on air Twitch's start times exact
Hours watched, average, peak reconstructed from sampling marked ~, with a coverage figure

Coverage is samples actually taken against samples a complete recording would hold. It is reported plainly rather than blended into a confidence score — one number that mixes unrelated things sounds precise and cannot be questioned.

It also states its own limit: coverage describes whether this application's sampling was complete. It claims nothing about whether Twitch's numbers were right.

What it does today

  • Live view — on air status, current viewers, peak so far, uptime
  • Current totals — followers and subscribers, fetched on demand
  • Daily history — a snapshot per metric per day, kept forever
  • Broadcast history — duration, peak and average viewers per stream, built from samples taken every minute while live
  • Monthly totals — a closed calendar month added up, with follower growth taken from two exact readings and audience figures carrying their coverage
  • Merging split broadcasts — Twitch issues a new stream id every time a connection drops, so one evening arrives as several. The collector notices, asks once the broadcasting is over, and merges only what you tick. Duration becomes the time actually on air, never the span including the outage

Conditions the numbers were collected under

Two things quietly change what stored data means, and neither shows up as an error on any card, so both are reported on the dashboard.

Clock drift. Almost every timestamp here comes from the local clock while the platform's come from theirs; near midnight a few minutes of drift files a metric under the wrong day. Every HTTP response carries the server's own time, so the offset is measured rather than assumed. It was found at 31 seconds on the machine this was built on.

Rate limiting. When Twitch refuses work, further requests are held locally until it says it will accept them again — the fastest way to stay blocked is to keep asking.

Twitch is implemented. The collector is platform-agnostic: adding another means writing one class and one line of registration.

Stack

Java 17 · Javalin · SQLite · JDBI · Flyway · Maven

Front end is plain HTML, CSS and JavaScript served by the same process. No npm, no bundler, no build step — the whole point is to stay light.

Deliberately not Spring: a thin layer over Jetty starts in about a second and the request flow is readable end to end. The reasoning behind this and every other choice is in docs/DECISIONS.md.

Running it

Requires Java 17 or newer.

mvn clean package
java -jar target/stream-metrics.jar

Then open http://localhost:7000.

It starts fine with no configuration at all and reports Twitch as not configured. To connect a channel:

  1. Register an application at https://dev.twitch.tv/console/apps
    • OAuth Redirect URL: http://localhost:7000/auth/twitch/callback
    • Client Type: Confidential
    • (Twitch requires two-factor authentication on the account to register one.)
  2. Copy config.properties.example to config.properties and fill in the client id and secret.
  3. Restart, open the dashboard, and click Connect Twitch.

config.properties is git-ignored. The application refuses to start if a credential is found in the versioned example file — see SecretGuard.

Scopes

moderator:read:followers and channel:read:subscriptions. Both require the broadcaster to log in personally — an app token is not enough, since Twitch removed the unauthenticated follower endpoint in 2023.

Data

Everything lives in one SQLite file, data/stream-metrics.db.

It is small: roughly 1 MB per year of daily snapshots and 4 MB per year of viewer samples. Ten years fit in about 50 MB. Minute-by-minute samples are summarised into their session and deleted after ninety days; daily snapshots are kept forever.

That file is irreplaceable. Platforms do not sell the past back — a lost snapshot is lost for good. Back it up with VACUUM INTO 'backup.db', which is safe to run while the application is going.

Licence

MIT.

About

Local social media metrics aggregator for streamers. Java, Javalin, SQLite. No number reaches the screen without saying how fresh it is.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages