#!/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

__TOTAL_LIBRARY__
Games in Library
0
Games Played
0h
Total Playtime

Top Games

# Game Playtime %

By Category

# Category Playtime %
""" def get_all_games(db_path: str) -> tuple[list[dict], int]: """Query the database and return all games with playtime and categories, plus total library count.""" conn = sqlite3.connect(db_path) cursor = conn.cursor() # Get total games in library cursor.execute("SELECT COUNT(*) FROM games") total_library = cursor.fetchone()[0] # Get games with playtime > 0 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) games = [ { "name": row[1], "playtime": row[2], "service": row[3], "categories": game_categories.get(row[0], []) } for row in games_rows ] return games, total_library def generate_report(db_path: str, output_path: str, top_n: int, bg_image_path: str = None) -> None: """Generate the HTML report.""" all_games, total_library = 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("__TOTAL_LIBRARY__", str(total_library)) 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 in library: {total_library}") 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())