Non-invasive playtime tracking Discord bot, compatible with all server types
  • Python 99.1%
  • Dockerfile 0.9%
Find a file
ultrablob 2a4fe8dd2f Escape player names for Discord markdown; make .env overrides opt-in
Names like _LLE123_ were italicized in embeds. Escape them everywhere
markdown renders (descriptions, field values, messages) — embed titles
don't render markdown, so they stay raw.

Comment out MC_LOG_DIR/PLAYTIME_DB in .env.example: env_file passes them
into the container, overriding the image's /logs and /data paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:05:24 -04:00
playtime_bot Escape player names for Discord markdown; make .env overrides opt-in 2026-07-03 20:05:24 -04:00
.dockerignore initial vibe commit 2026-07-03 19:39:35 -04:00
.env.example Escape player names for Discord markdown; make .env overrides opt-in 2026-07-03 20:05:24 -04:00
.gitignore initial vibe commit 2026-07-03 19:39:35 -04:00
docker-compose.yml initial vibe commit 2026-07-03 19:39:35 -04:00
Dockerfile initial vibe commit 2026-07-03 19:39:35 -04:00
main.py initial vibe commit 2026-07-03 19:39:35 -04:00
README.md initial vibe commit 2026-07-03 19:39:35 -04:00
requirements.txt initial vibe commit 2026-07-03 19:39:35 -04:00

Playtime Bot

A Discord bot that tracks player playtime on a Minecraft (Forge) server by reading the server logs — live-tailing latest.log and backfilling every rotated *.log.gz.

How it works

  • Backfill: on startup, every logs/*.log.gz is parsed once into join/leave sessions stored in SQLite (playtime.db). Processed files are remembered, so restarts are instant.
  • Live tail: latest.log is polled every 2 seconds. Completed sessions are written to the database immediately; players with an open session show as 🟢 online, and their in-progress time counts in every stat.
  • Rotation-safe: when the server restarts and latest.log rotates, dangling sessions are closed at the last log timestamp and the new .gz is ingested. Sessions are keyed on (player, start), so overlap between the live stream and the rotated file never double-counts.
  • Sessions left open when a server run ends (crash/stop) are closed at the file's last timestamp.

Setup

python -m venv .venv
.venv/bin/pip install -r requirements.txt
cp .env.example .env   # fill in DISCORD_TOKEN, adjust paths if needed
.venv/bin/python main.py

Create the bot at the Discord Developer Portal, enable no special intents (slash commands only), and invite it with the applications.commands + bot scopes.

To sanity-check ingestion without Discord:

.venv/bin/python main.py stats

Docker

cp .env.example .env   # fill in DISCORD_TOKEN
docker compose up -d --build

The compose file mounts ./logs read-only at /logs — point that volume at your Minecraft server's logs/ directory if it lives elsewhere. The SQLite database persists in the playtime-data named volume.

Set TZ (in .env or the shell) to the server's timezone; log timestamps are parsed as local time, so a mismatched container timezone shifts day boundaries and hourly stats. It defaults to America/Toronto.

Sanity-check inside the container:

docker compose run --rm playtime-bot python main.py stats

Commands

Command What it shows
/leaderboard [period] Playtime ranking — today, 7 days, 30 days, or all time
/playtime [player] Personal card: total, rank, today/week, sessions, average, longest, favorite hour, first/last seen
/online Who is on right now and for how long
/topdays [player] Most active calendar days (server-wide or per player)
/sessions [player] Longest single sessions ever
/streaks Longest consecutive-day play streaks, with current 🔥 streaks
/hours [player] ASCII histogram of activity by hour of day
/records Hall of fame: longest session, biggest day, peak concurrent players, longest streak, night owl 🦉, early bird 🌅
/link <player> Link your Discord account so /playtime defaults to you

Player arguments autocomplete from every name seen in the logs.

Notes

  • Log timestamps are interpreted in the machine's local timezone; day boundaries are local midnight, and sessions crossing midnight are split across days for daily stats.
  • The parser expects the Forge log format ([03Jul2026 16:28:06.262] ... <name> joined the game). Vanilla logs (time-only timestamps) are not supported.
  • Delete playtime.db to force a full re-ingest.