From a21e3f3d07698041c34062d9e68f1d0252a35dda 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 +++ generate_report.py | 673 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 716 insertions(+) create mode 100644 CLAUDE.md 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/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())