mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 19:27:04 +01:00
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
169 lines
5.7 KiB
Python
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!")
|