From f9d28b73f8817a348defd8bffd4106a4e993a476 Mon Sep 17 00:00:00 2001 From: Miguel Astor Date: Mon, 9 Feb 2026 13:12:17 -0400 Subject: [PATCH] Added code. --- CLAUDE.md | 43 +++ background.png | Bin 0 -> 24031 bytes generate_report.py | 673 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 716 insertions(+) create mode 100644 CLAUDE.md create mode 100644 background.png create mode 100644 generate_report.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7aba38c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Lutris Year in Review is a Python script that generates a static HTML playtime report from a Lutris gaming platform SQLite database. The report displays game playtime statistics with interactive Chart.js visualizations. + +## Commands + +Generate report with defaults: +```bash +python generate_report.py +``` + +Generate report with custom options: +```bash +python generate_report.py --db pga.db --output report.html --top 10 --background background.png +``` + +## Architecture + +**Single-file generator (`generate_report.py`):** +- Reads Lutris SQLite database (`pga.db`) containing games, categories, and playtime data +- Embeds all data (games JSON, background image as base64) directly into a self-contained HTML file +- HTML template with Chart.js doughnut chart and dynamic JavaScript filtering is embedded as a string constant (`HTML_TEMPLATE`) + +**Database schema (`schema.py`):** +- Reference file documenting Lutris database structure +- Key tables: `games` (with `playtime`, `service` fields), `categories`, `games_categories` (many-to-many join) + +**Generated output (`report.html`):** +- Fully static, can be hosted on any web server +- Client-side filtering by service (Steam, GOG, itch.io, local) +- Expandable "Others" row in games table +- Light/dark mode support via CSS `prefers-color-scheme` + +## Key Data Relationships + +- Games have a `service` field (steam, gog, itchio, humblebundle, or NULL for local) +- Games link to categories via `games_categories` join table +- Categories like `.hidden`, `favorite`, `Horny` are filtered out in the report +- `playtime` is cumulative hours (REAL), not per-session data diff --git a/background.png b/background.png new file mode 100644 index 0000000000000000000000000000000000000000..e7d85a22e903cff7b0ff35fbe369185e9ca9053a GIT binary patch literal 24031 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 Ef{E$o*Z=?k literal 0 HcmV?d00001 diff --git a/generate_report.py b/generate_report.py new file mode 100644 index 0000000..4cf4495 --- /dev/null +++ b/generate_report.py @@ -0,0 +1,673 @@ +#!/usr/bin/env python3 +"""Generate an HTML playtime report from a Lutris SQLite database.""" + +#################################################################################################### +# Copyright (C) 2026 by WallyHackenslacker wallyhackenslacker@noreply.git.hackenslacker.space # +# # +# Permission to use, copy, modify, and/or distribute this software for any purpose with or without # +# fee is hereby granted. # +# # +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS # +# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE # +# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, # +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # +# OF THIS SOFTWARE. # +#################################################################################################### + +import argparse +import base64 +import json +import sqlite3 +from pathlib import Path + +HTML_TEMPLATE = """ + + + + + Lutris Playtime Report + + + + +
+

Lutris Playtime Report

+
+ +
+
+
+ +
+
+
+
0
+
Games Played
+
+
+
0h
+
Total Playtime
+
+
+
+ +
+
+ +
+
+ +
+
+

Top Games

+
+ + + + + + + + + + +
#GamePlaytime%
+
+
+
+

By Category

+
+ + + + + + + + + + +
#CategoryPlaytime%
+
+
+
+ + + + +""" + + +def get_all_games(db_path: str) -> list[dict]: + """Query the database and return all games with playtime and categories.""" + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Get games with playtime + cursor.execute(""" + SELECT id, name, playtime, COALESCE(service, 'local') as service + FROM games + WHERE playtime > 0 + ORDER BY playtime DESC + """) + games_rows = cursor.fetchall() + + # Get categories for each game + cursor.execute(""" + SELECT gc.game_id, c.name + FROM games_categories gc + JOIN categories c ON gc.category_id = c.id + """) + categories_rows = cursor.fetchall() + conn.close() + + # Build game_id -> categories mapping + game_categories = {} + for game_id, category in categories_rows: + if game_id not in game_categories: + game_categories[game_id] = [] + game_categories[game_id].append(category) + + return [ + { + "name": row[1], + "playtime": row[2], + "service": row[3], + "categories": game_categories.get(row[0], []) + } + for row in games_rows + ] + + +def generate_report(db_path: str, output_path: str, top_n: int, bg_image_path: str = None) -> None: + """Generate the HTML report.""" + all_games = get_all_games(db_path) + + if not all_games: + print("No games with playtime found in the database.") + return + + total_playtime = sum(g["playtime"] for g in all_games) + total_games = len(all_games) + + # Load background image as base64 + bg_data_url = "" + if bg_image_path and Path(bg_image_path).exists(): + with open(bg_image_path, "rb") as f: + bg_base64 = base64.b64encode(f.read()).decode("utf-8") + bg_data_url = f"data:image/png;base64,{bg_base64}" + + html = HTML_TEMPLATE.replace("__ALL_GAMES__", json.dumps(all_games)) + html = html.replace("__TOP_N__", str(top_n)) + html = html.replace("__BACKGROUND_IMAGE__", bg_data_url) + + Path(output_path).write_text(html, encoding="utf-8") + print(f"Report generated: {output_path}") + print(f"Total games with playtime: {total_games}") + print(f"Total playtime: {total_playtime:.1f} hours") + + +def main(): + parser = argparse.ArgumentParser( + description="Generate an HTML playtime report from a Lutris database." + ) + parser.add_argument( + "--db", + default="pga.db", + help="Path to the Lutris SQLite database (default: pga.db)" + ) + parser.add_argument( + "--output", + default="report.html", + help="Output HTML file path (default: report.html)" + ) + parser.add_argument( + "--top", + type=int, + default=10, + help="Number of top games to show individually (default: 10)" + ) + parser.add_argument( + "--background", + default="background.png", + help="Path to background image (default: background.png)" + ) + + args = parser.parse_args() + + if not Path(args.db).exists(): + print(f"Error: Database file not found: {args.db}") + return 1 + + generate_report(args.db, args.output, args.top, args.background) + return 0 + + +if __name__ == "__main__": + exit(main())