#!/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
"""
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())