steam-backlog-enforcer/steam_backlog_enforcer/main/install.py
Krzysztof kuhy Rudnicki dc22bd557c fast install: never report a failed run as success; queue the manifest first
The BG3 fast install of 2026-10-23 downloaded 160 GB, was refused by the
installer, and the web job still ended on a full bar, "Done!" and Succeeded
while Steam restarted the download from 0%.

- A failed run's reason is read from the installer's own run log (its ERROR
  lines). A waiting job prints "Failed: fast install of <game>: <reason>",
  which fails the job; `install` no longer says "Done!" after it.
- Daemon-started failures stay on the Dashboard's Fast install card (and in
  ctl status, `error`) until a new run starts or the game is installed.
- install_game writes Steam's "needs downloading" manifest before the
  installer runs: it only finishes a manifest it finds, so fresh installs
  were refused for want of one.
- Command building moved to _fast_install_cmd (250-line cap).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYmCezqhUjUmMisF5YtM4F
2026-10-24 08:36:57 +02:00

169 lines
5.7 KiB
Python

"""Install, uninstall and library-visibility commands."""
from __future__ import annotations
from typing import TYPE_CHECKING
from steam_backlog_enforcer._actions import allowed_app_ids, allowed_games
from steam_backlog_enforcer._enforce_loop import get_all_owned_app_ids
from steam_backlog_enforcer._fast_install import fast_install_failure
from steam_backlog_enforcer._progress import current_progress
from steam_backlog_enforcer._prompter import (
PromptOption,
confirm_phrase,
current_prompter,
)
from steam_backlog_enforcer._steam_errors import SteamUnavailableError
from steam_backlog_enforcer._steam_state import is_game_fully_installed
from steam_backlog_enforcer.game_install import (
_echo,
get_installed_games,
install_game,
is_protected_app,
uninstall_other_games,
)
from steam_backlog_enforcer.library_hider import (
try_hide_other_games,
unhide_all_games,
)
if TYPE_CHECKING:
from steam_backlog_enforcer.config import Config, State
def cmd_installed(_config: Config, state: State) -> None:
"""Show installed games."""
installed = get_installed_games()
_echo(f"\nInstalled games ({len(installed)}):\n")
for app_id, name in installed:
protected = " [PROTECTED]" if is_protected_app(app_id) else ""
assigned = " <<< ASSIGNED" if app_id == state.current_app_id else ""
_echo(f" {app_id:>8d} {name}{protected}{assigned}")
def cmd_uninstall(_config: Config, state: State) -> None:
"""Uninstall all games except the assigned one."""
if state.current_app_id is None:
_echo("No game assigned. Run 'scan' first.")
return
allowed = allowed_app_ids(state)
installed = get_installed_games()
to_remove = [
(aid, n)
for aid, n in installed
if aid not in allowed and not is_protected_app(aid)
]
if not to_remove:
_echo("No games to uninstall (only allowed games and runtimes installed).")
return
_echo(f"\nWill uninstall {len(to_remove)} games, keeping:")
for aid, name in allowed_games(state):
_echo(f" - {name} (AppID={aid})")
_echo(" - Steam runtimes and Proton versions\n")
_echo("Games to remove:")
for aid, name in to_remove:
_echo(f" - {name} (AppID={aid})")
_echo()
if not confirm_phrase(
"uninstall", f"Uninstall {len(to_remove)} games?", count=len(to_remove)
):
_echo("Aborted.")
return
count = uninstall_other_games(allowed_app_ids(state))
_echo(f"\nUninstalled {count} games.")
def _pick_install_target(state: State) -> tuple[int, str] | None:
"""The allowed game to install: the only unfinished one, or the user's pick.
Every allowed game counts, not just the assignment: a manual pick whose
fast install was skipped (a game was running at pick time) needs a way to
start it later too.
"""
pending = [
(aid, n) for aid, n in allowed_games(state) if not is_game_fully_installed(aid)
]
if not pending:
_echo("All allowed games are already installed.")
return None
if len(pending) == 1:
return pending[0]
options = [PromptOption(str(aid), name or f"AppID={aid}") for aid, name in pending]
picked = int(current_prompter().choice("Install which game", options))
return next(game for game in pending if game[0] == picked)
def cmd_install(config: Config, state: State) -> None:
"""Install an allowed game, through the fast installer when it can be used.
A download Steam already started is taken over, so this is also how a
fast install that was skipped earlier gets started.
"""
if not allowed_games(state):
_echo("No game currently assigned. Run 'scan' first.")
return
target = _pick_install_target(state)
if target is None:
return
app_id, name = target
_echo(f"Installing {name} (AppID={app_id})...")
# install_game only *starts* the download (fast installer or steam://);
# there is no download-progress loop to report on yet.
current_progress().phase(f"Starting install of {name}")
if not install_game(app_id, name, config.steam_id, use_steam_protocol=True):
_echo("Failed to start the install.")
elif fast_install_failure(app_id) is None:
_echo("Done!")
else:
# The fast install already printed its "Failed: ..." line.
_echo("Steam's own download was started instead.")
def cmd_hide(config: Config, state: State) -> None:
"""Hide all non-assigned games in the Steam library."""
if state.current_app_id is None:
_echo("No game assigned. Run 'scan' first.")
return
owned_ids = get_all_owned_app_ids(config)
if not owned_ids:
_echo("No owned game list available. Run 'scan' first.")
return
_echo(f"Hiding all games except {state.current_game_name}...")
hidden, skipped = try_hide_other_games(owned_ids, allowed_app_ids(state))
if skipped is not None:
_echo(f"Library hiding: skipped ({skipped})")
return
_echo(f"Hidden {hidden} games.")
if hidden > 0:
_echo("Done! Only the assigned game should be visible in your library.")
def cmd_unhide(config: Config, _state: State) -> None:
"""Unhide all games in the Steam library."""
owned_ids = get_all_owned_app_ids(config)
if not owned_ids:
_echo("No owned game list available. Run 'scan' first.")
return
_echo("Unhiding all games...")
# Same contract as cmd_hide: no reachable Steam means nothing to change
# this time, not a failed command.
try:
count = unhide_all_games(owned_ids)
except SteamUnavailableError as exc:
_echo(f"Library unhiding: skipped ({exc})")
return
_echo(f"Unhidden {count} games.")
if count > 0:
_echo("Done!")