Files
Lutris-Playtime-Report-Gene…/generate_report.py
Miguel Astor 49c7a2bba8 Add brutalism template with bold industrial style
Features high-contrast colors, thick borders, hard shadows,
monospace typography, and no rounded corners. Uses custom
background placeholder that shows theme color when no image
is specified.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 03:18:13 -04:00

215 lines
8.7 KiB
Python

#!/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
# Directory where this script is located (for finding template.html)
SCRIPT_DIR = Path(__file__).parent
def load_template(template_file: str) -> str:
"""Load the HTML template from the specified file."""
template_path = SCRIPT_DIR / template_file
return template_path.read_text(encoding="utf-8")
def load_asset_as_base64(path: Path, mime_type: str) -> str:
"""Load a file and return it as a base64 data URL."""
if path.exists():
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8")
return f"data:{mime_type};base64,{data}"
return ""
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()
cursor.execute("SELECT COUNT(*) FROM games")
total_library = cursor.fetchone()[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()
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()
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, assets_dir: str, template_file: str, 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)
assets_path = Path(assets_dir)
# Load background image (custom or default stripes)
if bg_image_path and Path(bg_image_path).exists():
background_image = load_asset_as_base64(Path(bg_image_path), "image/png")
background_image_custom = f"url('{background_image}')"
else:
background_image = load_asset_as_base64(assets_path / "Others" / "stripes.png", "image/png")
background_image_custom = "none" # For templates that prefer no default background
# Load fonts
font_charcoal = load_asset_as_base64(assets_path / "Charcoal.ttf", "font/truetype")
font_monaco = load_asset_as_base64(assets_path / "MONACO.TTF", "font/truetype")
# Load images
titlebar_bg = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png")
title_stripes = load_asset_as_base64(assets_path / "Windows" / "title-1-active.png", "image/png")
close_btn = load_asset_as_base64(assets_path / "Windows" / "close-active.png", "image/png")
hide_btn = load_asset_as_base64(assets_path / "Windows" / "maximize-active.png", "image/png")
shade_btn = load_asset_as_base64(assets_path / "Windows" / "shade-active.png", "image/png")
check_off = load_asset_as_base64(assets_path / "Check-Radio" / "check-normal.png", "image/png")
check_on = load_asset_as_base64(assets_path / "Check-Radio" / "check-active.png", "image/png")
# Load scrollbar images
scrollbar_trough_v = load_asset_as_base64(assets_path / "Scrollbars" / "trough-scrollbar-vert.png", "image/png")
scrollbar_thumb_v = load_asset_as_base64(assets_path / "Scrollbars" / "slider-vertical.png", "image/png")
scrollbar_up = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-up.png", "image/png")
scrollbar_down = load_asset_as_base64(assets_path / "Scrollbars" / "stepper-down.png", "image/png")
# Load tab images
tab_active = load_asset_as_base64(assets_path / "Tabs" / "tab-top-active.png", "image/png")
tab_inactive = load_asset_as_base64(assets_path / "Tabs" / "tab-top.png", "image/png")
html = load_template(template_file)
html = html.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("__FONT_CHARCOAL__", font_charcoal)
html = html.replace("__FONT_MONACO__", font_monaco)
html = html.replace("__BACKGROUND_IMAGE__", background_image)
html = html.replace("__BACKGROUND_IMAGE_CUSTOM__", background_image_custom)
html = html.replace("__TITLEBAR_BG__", titlebar_bg)
html = html.replace("__TITLE_STRIPES__", title_stripes)
html = html.replace("__CLOSE_BTN__", close_btn)
html = html.replace("__HIDE_BTN__", hide_btn)
html = html.replace("__SHADE_BTN__", shade_btn)
html = html.replace("__CHECK_OFF__", check_off)
html = html.replace("__CHECK_ON__", check_on)
html = html.replace("__SCROLLBAR_TROUGH_V__", scrollbar_trough_v)
html = html.replace("__SCROLLBAR_THUMB_V__", scrollbar_thumb_v)
html = html.replace("__SCROLLBAR_UP__", scrollbar_up)
html = html.replace("__SCROLLBAR_DOWN__", scrollbar_down)
html = html.replace("__TAB_ACTIVE__", tab_active)
html = html.replace("__TAB_INACTIVE__", tab_inactive)
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(
"--assets",
default="Platinum",
help="Path to Platinum assets directory (default: Platinum)"
)
parser.add_argument(
"--background",
default=None,
help="Path to background image for tiling (default: Platinum stripes pattern)"
)
parser.add_argument(
"--template",
default="platinum.html",
help="HTML template file to use (default: platinum.html)"
)
args = parser.parse_args()
if not Path(args.db).exists():
print(f"Error: Database file not found: {args.db}")
return 1
if not Path(args.assets).exists():
print(f"Error: Assets directory not found: {args.assets}")
return 1
template_path = SCRIPT_DIR / args.template
if not template_path.exists():
print(f"Error: Template file not found: {template_path}")
return 1
generate_report(args.db, args.output, args.top, args.assets, args.template, args.background)
return 0
if __name__ == "__main__":
exit(main())