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]

Download RAW File