1
#!/root/reolink/bin/python3
import os
import re
import sys
import time
import json
import argparse
import subprocess
import shutil
from pathlib import Path
from rich import box
from rich.console import Console
from rich.prompt import Prompt
from rich.table import Table
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
GAMES_ROOT = Path("/mnt/storage/games")
INFO_FILE = "game_info"
STEAMCMD = "/usr/games/steamcmd"
STEAM_USER = "anonymous"
STEAMDB_URL_PREFIX = "https://steamdb.info/app/"
DEBUG = False
console = Console()
def debug(message):
if DEBUG:
console.print(f"[dim cyan][DEBUG] {message}[/dim cyan]")
def find_games(root: Path):
games = []
for d in root.iterdir():
if d.is_dir():
info = d / INFO_FILE
if info.is_file():
games.append((d.name, info))
return games
def parse_game_info(path: Path):
data = {}
try:
content = path.read_text()
debug(f"Raw game_info content:\n{content}")
for line in content.splitlines():
if m := re.match(r"^(build|steamid|csrinru):\s*(.+)$", line):
data[m.group(1)] = m.group(2).strip()
debug(f"Parsed game_info data: {data}")
if "build" in data:
data["build"] = int(data["build"])
debug(f"Converted build to int: {data['build']}")
return data
except Exception as e:
console.print(f"[bold red]Error parsing {path}: {e}[/bold red]")
return data
def batch_get_latest_builds(steam_ids):
try:
steam_cmd_path = shutil.which(STEAMCMD)
if not steam_cmd_path:
console.print(
f"[bold red]Error: steamcmd not found in PATH. Please install steamcmd or provide full path in the CONFIG section.[/bold red]"
)
return {}
cmd = ["sudo", "-u", "steam", STEAMCMD, "+login", STEAM_USER]
for steamid in steam_ids:
cmd.append(f"+app_info_print")
cmd.append(steamid)
cmd.append("+quit")
debug(f"Running batch command: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
console.print(f"[bold red]Error running steamcmd batch request[/bold red]")
debug(f"Return code: {result.returncode}")
debug(f"Stderr: {result.stderr}")
return {}
output = result.stdout
debug_file = Path(f"/tmp/steamcmd_batch_output.txt")
debug_file.write_text(output)
debug(f"Full steamcmd output saved to: {debug_file}")
build_ids = {}
for steamid in steam_ids:
app_section_pattern = f"AppID : {steamid}.*?(\n\n|\Z)"
app_section_match = re.search(app_section_pattern, output, re.DOTALL)
if not app_section_match:
debug(f"Could not find section for app {steamid}")
continue
app_section = app_section_match.group(0)
patterns = [
r'"public"\s*{\s*"buildid"\s*"(\d+)"',
r'"branches"\s*{[^}]*"public"\s*{[^}]*"buildid"\s*"(\d+)"',
r'"buildid"\s*"(\d+)".*?"public"',
]
for pattern in patterns:
match = re.search(pattern, app_section, re.DOTALL)
if match:
build_ids[steamid] = int(match.group(1))
debug(f"Found build ID for {steamid}: {build_ids[steamid]}")
break
return build_ids
except Exception as e:
console.print(f"[bold red]Error getting latest builds: {e}[/bold red]")
return {}
def update_game_info(info_path: Path, new_build: int) -> bool:
try:
lines = info_path.read_text().splitlines()
debug(f"Original lines: {lines}")
new_lines = [
re.sub(r"^build:\s*\d+", f"build: {new_build}", line)
if line.startswith("build:")
else line
for line in lines
]
debug(f"Updated lines: {new_lines}")
info_path.write_text("\n".join(new_lines) + "\n")
return True
except Exception as e:
console.print(f"[bold red]Error updating {info_path}: {e}[/bold red]")
return False
def display_games_list(all_games, latest_builds):
while True:
console.clear()
console.rule("[bold green]Game List[/]")
sorted_games = sorted(all_games, key=lambda x: x["name"].lower())
table = Table(
box=box.ROUNDED,
title="Complete Games List",
expand=True,
border_style="cyan",
show_header=True,
header_style="bold",
show_lines=True,
)
table.add_column("#", style="cyan", no_wrap=True)
table.add_column("Game", style="magenta")
table.add_column("Build", justify="right")
table.add_column("SteamDB", style="blue")
table.add_column("CS.RIN.RU", style="cyan")
for i, game in enumerate(sorted_games, 1):
current_build = game["current"]
steamid = game["steamid"]
latest_build = latest_builds.get(steamid, 0)
if latest_build > 0 and current_build != latest_build:
build_style = "red"
else:
build_style = "green"
table.add_row(
str(i),
game["name"],
f"[{build_style}]{current_build}[/{build_style}]",
f"[link={STEAMDB_URL_PREFIX}{steamid}]{STEAMDB_URL_PREFIX}{steamid}[/link]",
f"[link={game['csrinru']}]{game['csrinru']}[/link]"
if game["csrinru"] != "N/A"
else "N/A",
)
table.caption = f"Total Games: {len(sorted_games)}"
console.print(table)
console.print("\n[dim](m) Main menu (r) Refresh (q) Quit[/dim]")
choice = Prompt.ask(
"\nCommand[m/r/q]",
choices=["m", "r", "q"],
default="r",
)
if choice.lower() == "q":
console.print("[bold green]Done.[/bold green]")
sys.exit(0)
elif choice.lower() == "m":
return
elif choice.lower() == "r":
games = find_games(GAMES_ROOT)
all_games = []
game_metadata = {}
steam_ids = set()
for name, info_path in games:
meta = parse_game_info(info_path)
if "steamid" not in meta or "build" not in meta:
continue
steamid = meta["steamid"]
steam_ids.add(steamid)
game_metadata[name] = {
"name": name,
"info": str(info_path),
"current": meta["build"],
"steamid": steamid,
"csrinru": meta.get("csrinru", "N/A"),
}
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]Refreshing game information..."),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeElapsedColumn(),
console=console,
transient=True,
expand=True,
) as progress:
progress_task = progress.add_task("Fetching updates", total=1)
latest_builds = batch_get_latest_builds(list(steam_ids))
progress.update(progress_task, completed=1)
for name, meta in game_metadata.items():
steamid = meta["steamid"]
current_build = meta["current"]
if steamid in latest_builds:
latest = latest_builds[steamid]
meta["latest"] = latest
meta["needs_update"] = latest > 0 and latest != current_build
else:
meta["latest"] = 0
meta["needs_update"] = False
all_games.append(meta)
sorted_games = sorted(all_games, key=lambda x: x["name"].lower())
continue
else:
return
def display_game_details(game, latest_build):
console.clear()
console.rule(f"[bold green]Game Details: {game['name']}[/]")
details_table = Table(
box=box.ROUNDED,
title="Game Information",
expand=False,
border_style="cyan",
show_header=False,
show_lines=True,
)
details_table.add_column(style="bold")
details_table.add_column()
details_table.add_row("Game", f"[magenta]{game['name']}[/magenta]")
details_table.add_row("Current Build", f"[yellow]{game['current']}[/yellow]")
if latest_build > 0:
details_table.add_row("Latest Build", f"[green]{latest_build}[/green]")
else:
details_table.add_row("Latest Build", "[yellow]Unknown[/yellow]")
details_table.add_row(
"SteamDB",
f"[blue][link={STEAMDB_URL_PREFIX}{game['steamid']}]{STEAMDB_URL_PREFIX}{game['steamid']}[/link][/blue]"
)
details_table.add_row(
"CS.RIN.RU",
f"[cyan][link={game['csrinru']}]{game['csrinru']}[/link][/cyan]"
if game["csrinru"] != "N/A"
else "[cyan]N/A[/cyan]",
)
console.print(details_table)
needs_update = latest_build > 0 and game['current'] < latest_build
if needs_update:
console.print("\n[dim](m) Main menu (q) Quit[/dim]")
confirm = Prompt.ask(
f"\nUpdate [magenta]{game['name']}[/magenta] from [yellow]{game['current']}[/yellow] to [green]{latest_build}[/green]?",
choices=["y", "n", "m", "q"],
default="n",
)
if confirm.lower() == "y":
if update_game_info(Path(game["info"]), latest_build):
console.print(
f"[bold green]✓ Updated[/bold green] {game['name']} → build {latest_build}"
)
game['current'] = latest_build
else:
console.print(
f"[bold red]✗ Failed to update[/bold red] {game['name']}"
)
time.sleep(1.5)
return "main"
elif confirm.lower() == "q":
console.print("[bold green]Done.[/bold green]")
sys.exit(0)
elif confirm.lower() == "m":
return "main"
else:
return "main"
else:
console.print("\n[dim](m) Main menu (q) Quit[/dim]")
choice = Prompt.ask(
"\nCommand[m/q]",
choices=["m", "q"],
default="m",
)
if choice.lower() == "q":
console.print("[bold green]Done.[/bold green]")
sys.exit(0)
else:
return "main"
def path_to_str(obj):
if isinstance(obj, Path):
return str(obj)
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
def main():
parser = argparse.ArgumentParser(description="Game Updates Checker")
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
args = parser.parse_args()
global DEBUG
DEBUG = args.debug
title = "Game Updates Checker"
if DEBUG:
title += " (Debug Mode)"
while True:
console.clear()
console.rule(f"[bold green]{title}[/]")
games = find_games(GAMES_ROOT)
game_count = len(games)
debug(f"Found {game_count} games")
all_games = []
game_metadata = {}
steam_ids = set()
for name, info_path in games:
meta = parse_game_info(info_path)
if "steamid" not in meta or "build" not in meta:
if DEBUG:
missing = []
if "steamid" not in meta:
missing.append("Steam ID")
if "build" not in meta:
missing.append("build number")
console.print(
f"[yellow]Warning: Missing {', '.join(missing)} for {name}[/yellow]"
)
continue
steamid = meta["steamid"]
steam_ids.add(steamid)
game_metadata[name] = {
"name": name,
"info": str(info_path),
"current": meta["build"],
"steamid": steamid,
"csrinru": meta.get("csrinru", "N/A"),
}
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]Checking for updates..."),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeElapsedColumn(),
console=console,
transient=True,
expand=True,
) as progress:
progress_task = progress.add_task("Fetching updates", total=1)
latest_builds = batch_get_latest_builds(list(steam_ids))
progress.update(progress_task, completed=1)
updates = []
for name, meta in game_metadata.items():
steamid = meta["steamid"]
current_build = meta["current"]
if steamid in latest_builds:
latest = latest_builds[steamid]
meta["latest"] = latest
meta["needs_update"] = latest > 0 and latest != current_build
if meta["needs_update"]:
updates.append(meta)
else:
meta["latest"] = 0
meta["needs_update"] = False
all_games.append(meta)
debug(f"All games info: {json.dumps(all_games, indent=2)}")
debug(f"Updates needed: {json.dumps(updates, indent=2)}")
console.print("\n")
if not updates:
console.print(
Panel(
"[bold green]All games are up to date![/bold green]",
title="Game Updates Status",
border_style="green",
expand=False,
padding=(2, 4)
)
)
console.print("\n[dim](r) Refresh (l) List all games (q) Quit[/dim]")
choice = Prompt.ask("\nCommand[r/l/q]", choices=["r", "l", "q"], default="r")
if choice.lower() == "q":
console.print("[bold green]Done.[/bold green]")
sys.exit(0)
elif choice.lower() == "l":
display_games_list(all_games, latest_builds)
continue
elif choice.lower() == "r":
continue
else:
continue
else:
update_count = len(updates)
table = Table(
box=box.ROUNDED,
title="Updates Available",
expand=False,
border_style="cyan",
show_header=True,
header_style="bold",
show_lines=True,
)
table.add_column("#", style="cyan", no_wrap=True)
table.add_column("Game", style="magenta")
table.add_column("Current Build", style="yellow", justify="right")
table.add_column("Latest Build", style="green", justify="right")
table.add_column("SteamDB", style="blue")
table.add_column("CS.RIN.RU", style="cyan")
for i, update in enumerate(updates, 1):
table.add_row(
str(i),
update["name"],
str(update["current"]),
str(update["latest"]),
f"[link={STEAMDB_URL_PREFIX}{update['steamid']}]{STEAMDB_URL_PREFIX}{update['steamid']}[/link]",
f"[link={update['csrinru']}]{update['csrinru']}[/link]"
if update["csrinru"] != "N/A"
else "N/A",
)
console.print(table)
console.print(f"\n[dim](1-{update_count}) Select game (r) Refresh (l) List all games (q) Quit[/dim]")
num_choices = [str(i) for i in range(1, len(updates) + 1)]
all_choices = num_choices + ["r", "l", "q"]
choice = Prompt.ask(
"\nCommand[#/r/l/q]",
choices=all_choices,
default="r",
)
if choice.lower() == "q":
console.print("[bold green]Done.[/bold green]")
sys.exit(0)
if choice.lower() == "l":
display_games_list(all_games, latest_builds)
continue
if choice.lower() == "r":
continue
if choice.isdigit() and 1 <= int(choice) <= len(updates):
sel = updates[int(choice) - 1]
display_game_details(sel, sel["latest"])
continue
continue
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
console.print("\n[bold yellow]Program terminated by user.[/bold yellow]")
sys.exit(0)
except Exception as e:
console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
if DEBUG:
import traceback
console.print(f"[dim red]{traceback.format_exc()}[/dim red]")
sys.exit(1)
For immediate assistance, please email our customer support: [email protected]