A local, Backloggd-style app for managing your video game backlog from
your existing Excel/CSV file, "Completed" / "Backlog" grids, notes,
markdown reviews, cover art, and export to .xlsx/.csv at any time. Your data always stays
yours.
Apps like backloggd doesnt let you export to an xlsx so if you loose your mail or the site disappear, your backlog will too.
Nobody should have to pay for this, or even give credientials really ...
Works identically on Windows and Linux, and can also run via Docker.
Download the zip file and extract it : here.
You can also clone the repository :
git clone https://github.com/Matth-L/MyBacklog.gitWindows/Linux (no Docker) — requires Python 3.10+:
./my_backlog_linux.sh # Linux/macOS
my_backlog_windows.bat # Windows (double-click, or run from a terminal)This creates a virtual environment, installs dependencies, starts the server, and opens
http://127.0.0.1:5000 in your browser.
I created an example.xslx file to show how this app is supposed to work. Don't hesitate to import it to test the app.
Docker:
docker compose up -d --buildThen open http://localhost:5000.
- Import/export: your
My_Backlog.xlsxor 3 CSVs (Backlog/Reviews/Completed), preserving your original file's logic (year markers, ignored summary columns, bold/italic reviews converted to markdown). Export anytime to a formatted.xlsxor a CSV zip. Import another session at any time, or start empty. - Dashboard: hours played, ratings, pace (avg hours/month, time to clear your backlog), longest/shortest/best/worst-rated game, yearly progress, monthly trend, rating distribution, DLC counts, and a "My Year Review" Wrapped-style annual summary.
- Grids: SteamGridDB-style covers, tile-size slider, filters (availability, year, DLC, abandoned), sort modes, year separators, and a Random Pick animation to decide what to play next.
- Cover art: search across Steam/RAWG/Giant Bomb/Wikipedia, or upload your own — either way, an interactive Discord/GitHub-style editor (crop, fill, stretch, drag, zoom) Every fetched image is validated before being accepted. Nintendo games won't work, they are only available using IGBD, and I don't really want to create an account with 2 steps auth for that.
- HowLongToBeat: fetch an estimated playtime on demand (Main/Main+Extra/Completionist), never automatically.
- Sanitize Game Names: suggests canonical titles (fixing abbreviations/incomplete names)
- Backups: triggered only by explicit actions (new game, edited review, import) — never a
background timer. Each backup writes a timestamped
.xlsx+.csvtobackup_backlog/; the 3 most recent of each are kept. Restorable from Settings.
Cover art search can query the following, only when you use it explicitly and only after you consent:
- Steam Grid DB : free API key (optional, add it in Settings); skipped without one. Community-uploaded cover art in the exact portrait "grid" dimensions Steam uses.
- Steam Store : public search, no key needed.
- RAWG.io : free API key (optional, add it in Settings); skipped without one.
- Giant Bomb : free API key (optional, add it in Settings); skipped without one.
- TheGamesDB : free API key (optional, add it in Settings); skipped without one. Open, community-maintained database with strong retro/console box-art coverage.
- Wikipedia : last-resort fallback if nothing else is found.
Steam is a trademark of Valve Corporation. RAWG.io, Giant Bomb, and Wikipedia belong to their respective owners. Fetched images/metadata belong to their owners; MyBacklog doesn't redistribute them and has no commercial purpose — everything is stored locally, for your personal use only.
data/backlog.db,data/covers/,data/config.json— database, covers, settings (created next toapp.py, or in/app/datawith Docker).cover_art/— your own manually-dropped covers, at the project root.backup_backlog/—.xlsx/.csvbackups, at the project root.
Nothing is sent anywhere except the cover-art lookups described above, and only when you trigger them.
Flask (single process, no build step) + vanilla JS/HTML/CSS frontend + SQLite. No ORM, no
frontend framework, no bundler — static/js/app.js is loaded directly by the browser.
app.py Flask routes
backend/
db.py SQLite connection, schema + migrations
importer.py Excel/CSV import, review-matching pipeline
exporter.py xlsx/csv export (incl. formula-injection neutralization)
covers.py Cover art search (Steam/RAWG/Giant Bomb/Wikipedia) + local cache
sanitize.py Canonical-name suggestion pipeline + hash-based cache
sequel_guard.py Shared "don't confuse Dark Souls II with III" guard
duplicates.py Backlog/Completed duplicate detection (computed live, never cached)
hltb.py howlongtobeatpy wrapper
stats.py Dashboard + Year Review aggregation
backup.py / session.py Backup/restore, import-a-new-session-at-any-time
applog.py Curated terminal logging (no per-request access log spam)
data/aliases_seed.json Bundled canonical-name dictionary (seed for sanitize.py)
static/
js/app.js All frontend logic
js/translations/ fr.js / en.js (add a locale by copying one + registering in index.js)
css/style.css
tests/ pytest, one file per backend module
pip install -r requirements.txt
pytest tests/ -q| Variable | Default | Purpose |
|---|---|---|
BACKLOG_DATA_DIR |
./data |
DB, covers, config.json |
BACKLOG_BACKUP_DIR |
./backup_backlog |
Backup files |
BACKLOG_COVER_ART_DIR |
./cover_art |
User-dropped covers |
BACKLOG_NO_BROWSER |
unset | Skip auto-opening a browser tab on startup |
PORT |
5000 |
Server port |
- Matching order (reviews, cover art, name sanitization): exact local match → alias →
normalized → fuzzy → external API, always local-first. A fuzzy/external suggestion is never
auto-applied — it's surfaced for confirmation.
sequel_guard.pypenalizes/rejects candidates whose extracted sequel number (roman or arabic) disagrees, since plain text similarity can't tell II from III apart. - Offline-first:
sanitize.pyfolds confident external-API hits into a localdata/aliases_learned.jsoncache, so the app gets faster and more offline-capable the more it's used. No external service is ever required for core functionality. - Security: all SQL is parameterized; user-controlled text (titles/reviews/notes) is escaped
via
escapeHtml()before anyinnerHTMLinterpolation; exports neutralize Excel/CSV formula injection (leading=/+/-/@get a literal-text prefix); path-accepting endpoints reject traversal attempts;defusedxmlis pinned explicitly for XXE protection inopenpyxl;MAX_CONTENT_LENGTHcaps request body size. - No background jobs: backups, cover-art bulk-fill, and sanitize scans all run in a
daemon thread on explicit user action, with a polled
/statusendpoint for progress — nothing runs on a timer.
Disclaimer: This app was created with the help of AI.
