A community-activity monitor for Pokémon GO. It fetches upcoming and live events, stores them locally, and shows them through two interfaces that share one design system — a CustomTkinter desktop app and a Flask web app, both in a consistent Midnight Blue dark theme.
Important
Unofficial fan project. PokéTrack is not affiliated with, endorsed by, or sponsored by Niantic, Nintendo, The Pokémon Company, or Leek Duck. "Pokémon" and "Pokémon GO" are trademarks of their respective owners, used here for identification only. Event data is sourced from Leek Duck via ScrapedDuck for informational, non-commercial use. Software provided "AS IS", without warranty. See DISCLAIMER.md and LICENSE.
| Data source | Robust parser over the ScrapedDuck JSON feed (a structured Leek Duck mirror), plus a best-effort official-blog HTML source behind the same interface. |
| Hybrid GUI | Desktop (CustomTkinter) + Web (Flask + Tailwind), both Midnight Blue dark mode driven by one shared palette. |
| i18n | English (default), Traditional Chinese, Simplified Chinese, Japanese, Korean — every string lives in languages.json. |
| Region filtering | Pick the regions you care about; Global events always show. Rules live in editable data/regions_map.json. |
| Search & filters | Live search + event-type filter + favorites in both UIs, on top of the region filter. |
| Rich cards & detail view | Thumbnails, synthesised descriptions, live countdowns, a live/upcoming/total stats bar, and a click-through event detail view (desktop modal / web page). |
| Favorites | Star event types; filter to favorites and optionally notify only for favorites. |
| Calendar export | Per-event or filtered .ics export, plus a subscribable web feed at /calendar.ics. |
| Notifications | Desktop + in-app alerts, webhooks (Discord/Slack/custom, optionally HMAC-signed), and Telegram — when new events appear in your regions. |
| Config import/export | Back up or move your settings from either UI. |
| Async + responsive | async/await (httpx) fetches off the GUI thread, with a skeleton loading screen. |
| Native fast path | Optional Rust extension (PyO3) parses the feed + classifies regions in native code, with automatic pure-Python fallback when it isn't installed. |
| Persistence | SQLite (indexed) caches events for offline viewing; old events auto-pruned. |
| Background updates | APScheduler on a configurable interval; the web view auto-detects new events. |
| Distribution | CI (pytest matrix), a Dockerfile for the web app, and a one-file Windows .exe built on release. |
| Graceful errors | Network/format failures never crash the UI — cached data stays visible with a localized message. |
| Tested | Deterministic, offline pytest suite (27 tests) covering core, service, web, calendar, favorites, HMAC, and the scheduler. |
PokéTrack App/
├── main.py # entry point → desktop GUI
├── run_web.py # entry point → web GUI
├── config.json # user settings (language, regions, interval, webhook, …)
├── languages.json # ALL UI strings (en / zh-Hant / zh-Hans)
├── requirements.txt
├── LICENSE # MIT (original code only)
├── DISCLAIMER.md # trademarks, data attribution, no-warranty notice
├── data/ # SQLite DB + image cache (auto-created)
├── tests/ # offline pytest suite
└── poketrack/
├── app_context.py # builds the shared service (config+db+parser+scheduler)
├── config.py # config.json manager
├── i18n.py # Translator (dotted keys + fallback)
├── core/ # data layer — NO UI imports
│ ├── models.py # Event model + status/countdown logic
│ ├── regions.py # region constants + keyword classifier
│ ├── database.py # SQLite persistence (thread-safe, WAL, pruning, migrations)
│ ├── http.py # shared requests.Session with retries/backoff
│ ├── parser.py # LeekDuck (JSON) + Blog (HTML) sources
│ ├── native.py # loader for the optional Rust fast path (guarded)
│ ├── notify.py # optional desktop notifications (guarded)
│ ├── webhook.py # outgoing webhooks (Discord/Slack/generic)
│ ├── scheduler.py # APScheduler wrapper
│ └── service.py # PokeTrackService — the shared controller
├── gui/ # desktop presentation
│ ├── theme.py # Midnight Blue palette — single source of truth
│ ├── images.py # async thumbnail loader (off-thread, cached)
│ └── app.py # CustomTkinter app
└── web/ # web presentation
├── server.py # Flask app + JSON API
├── templates/ # base.html (Tailwind config injected) + index.html
└── static/ # css/style.css, js/app.js
poketrack-native/ # optional Rust extension (PyO3) — feed parse + region classify
├── Cargo.toml
├── pyproject.toml # maturin build backend
├── src/lib.rs
└── benchmark.py # native vs pure-Python benchmark
web-frontend/ # TypeScript interactive layer for the web UI (Vite build)
├── package.json
├── tsconfig.json
├── vite.config.ts
└── src/ # main.ts, api.ts, countdown.ts, search.ts, favorites.ts …
# → compiles to poketrack/web/static/dist/app.js (committed)
Polyglot by design. Python orchestrates and renders; an optional Rust extension accelerates the data path; the web front-end is TypeScript. The Rust and TypeScript layers each degrade/rebuild independently, and the app runs from a plain checkout with neither a Rust nor a Node toolchain — those are only needed to rebuild the compiled artifacts, which are committed.
┌──────────────────────────────┐
│ PokeTrackService │ ← the only thing both UIs touch
│ (core/service.py) │
└───┬───────────┬───────────┬───┘
parser.py │ database │ scheduler│
(LeekDuck/Blog)│ (SQLite) │ (APSched) │
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐
│ gui/app.py │ │ web/server.py │
│ CustomTkinter │ │ Flask + Tailwind │
└───────────────┘ └──────────────────┘
▲ ▲
└──── theme.py ─────┘ (one Midnight Blue palette)
The fetching/parsing logic is fully separated from presentation: both
front-ends call PokeTrackService only, and neither imports the parser or
database directly.
pip install -r requirements.txtPython 3.10+ recommended (tested on 3.13 and 3.14).
Pillow(thumbnails) andplyer(desktop notifications) are used with graceful fallback — if either is missing the app still runs, just without that one feature.
Multiple Python installs? Make sure you install into the same interpreter you run with. A virtual environment avoids the mix-up entirely:
python -m venv .venv && .venv\Scripts\activate # Windows pip install -r requirements.txt
Desktop app
python main.pyWeb app
python run_web.py
# then open http://127.0.0.1:5000/The desktop app's “Open Web View” button launches the web server for you and opens your browser.
python -m pytest -qThe suite is fully offline (a fake source + temp DB) and won't touch your real
config.json or data/. It also runs in CI on every push (Python 3.11–3.13).
Docker (web app)
docker build -t poketrack .
docker run -p 5000:5000 poketrack # http://localhost:5000/Standalone Windows executable
pip install pyinstaller
pyinstaller --noconfirm PokeTrack.spec # -> dist/PokeTrack.exeOn GitHub, publishing a Release triggers .github/workflows/release.yml, which
builds PokeTrack.exe and attaches it to that release automatically.
PokéTrack has an optional Rust extension (poketrack-native/, built with
PyO3 + maturin) that parses the
ScrapedDuck feed and classifies regions in native code. It is not required —
poketrack/core/native.py imports it if present and transparently falls back to
the pure-Python path if not, so the app (and the whole test suite) behaves
identically either way.
pip install maturin
pip install ./poketrack-native # compile + install the extension
python poketrack-native/benchmark.py # compare against the pure-Python pathThe build produces an abi3 wheel (cp39-abi3), so a single wheel works on
any CPython ≥ 3.9 — no rebuild per interpreter. Measured on the live feed, the
native JSON→structured-data step is ~3–5× faster; end-to-end the gain is smaller
because building Python Event objects (unavoidable Python work) dominates —
the benchmark reports both numbers honestly. Region classification and datetime
normalisation keep identical semantics to the Python path (verified by a
parity test in tests/).
The web UI's interactive layer is TypeScript, bundled by
Vite into poketrack/web/static/dist/app.js. It progressively
enhances the Flask server-rendered page — the page works with JavaScript
disabled; the bundle adds live-ticking countdowns, instant client-side search,
no-reload favorite toggling, and the async refresh/poller.
cd web-frontend
npm install
npm run build # type-check + emit the committed bundle
npm run dev # optional: Vite dev serverThe compiled bundle is committed, so running the app needs no Node — only rebuilding does. CI type-checks and builds it on every push.
Set webhook_url (in config.json or the desktop Settings tab) to receive a
POST whenever new events appear in your selected regions. The payload is shaped
automatically for the destination:
| URL contains | Format sent |
|---|---|
discord.com/api/webhooks/… |
Discord { content, embeds[] } |
hooks.slack.com/… |
Slack { text } |
| anything else | Generic { content, text, title, events[] } |
The Settings tab has a Send test button to verify your URL.
Everything here is also editable in the app's Settings tab.
config.jsonis git-ignored (it can hold a private webhook URL). Aconfig.example.jsonis provided for reference; the app auto-createsconfig.jsonfrom defaults on first run.
All UI text is loaded from languages.json, grouped by language then by section:
{ "en": { "events": { "view_details": "View Details" } } }Code looks strings up by dotted key with graceful fallback (current language → English → the key itself):
service.t("events.view_details")
service.t("events.last_updated", time="…") # supports {placeholders}To add a language: copy the "en" block to a new top-level key (e.g.
"ja"), translate the values, and add its display name under every
languages block. No code changes needed.
Global, North America, South America, Europe, Asia, Oceania,
Africa.
Leek Duck data isn't region-tagged, so PokéTrack infers a region from the event
name using a keyword map (core/regions.py) — Safari Zones / GO Tour stops map
to their city/continent; everything else stays Global and shows for
everyone. Selecting a region (e.g. Asia) shows Global + Asia. Extend the
keyword map freely as new regional events appear.
Deep blues, charcoal grays, and slate accents, defined once in
poketrack/gui/theme.py. The desktop UI reads the hex
values directly; the web layer injects the same values into its Tailwind
config and CSS variables (web/templates/base.html), so the two interfaces stay
identical.
| Token | Hex | Use |
|---|---|---|
bg |
#0B1120 |
App background |
surface |
#111827 |
Cards / panels |
surface_alt |
#1E293B |
Elevated surfaces |
border |
#334155 |
Slate borders |
primary |
#3B82F6 |
Primary actions |
accent |
#38BDF8 |
Highlights |
text / text_muted |
#E2E8F0 / #94A3B8 |
Text |
success / warning / danger |
#34D399 / #FBBF24 / #F87171 |
LIVE / SOON / errors |
- First launch shows an empty list for a second while the initial fetch runs in the background; it fills in automatically.
- Offline? Cached events from the last successful fetch remain visible and a localized "showing cached data" message appears.
- The default data source depends on a public community feed; if it's
unreachable, switch
"source"to"blog"inconfig.json.
- Event data: Leek Duck, via the ScrapedDuck dataset. Please support the original source.
- Built with CustomTkinter, Flask, APScheduler, and Tailwind CSS.
- Original code: released under the MIT License. The MIT grant covers this project's source code only.
- Trademarks & third-party data: Pokémon / Pokémon GO intellectual property and Leek Duck event data are not covered by the MIT license and remain the property of their respective owners. This is an unofficial, non-commercial fan project with no affiliation or endorsement, provided "AS IS" without warranty.
- Full terms: DISCLAIMER.md.
Replace the copyright holder in
LICENSEwith your name/handle before publishing if you'd like attribution.
{ "language": "en", // "en" | "zh-Hant" | "zh-Hans" "regions": ["Global"], // see region list below "source": "leekduck", // "leekduck" (default) | "blog" "refresh_interval_minutes": 60, // APScheduler interval "notifications": true, // desktop/in-app alerts for new events "webhook_url": "", // POST new-event alerts here (Discord/Slack/custom) "prune_after_days": 45, // drop events that ended over N days ago "database_path": "data/poketrack.db", "web": { "host": "127.0.0.1", "port": 5000, "debug": false } }