mirror of
https://github.com/derrod/legendary.git
synced 2026-01-08 14:12:45 +00:00
feat: basics in non interactive mode
This commit is contained in:
parent
64b99ddb6f
commit
41aa2b1a66
145
legendary/cli.py
145
legendary/cli.py
|
|
@ -16,6 +16,7 @@ from logging.handlers import QueueListener
|
|||
from multiprocessing import freeze_support, Queue as MPQueue
|
||||
from platform import platform
|
||||
from sys import exit, stdout, platform as sys_platform
|
||||
from epic_expreval import Tokenizer
|
||||
|
||||
from legendary import __version__, __codename__
|
||||
from legendary.core import LegendaryCore
|
||||
|
|
@ -27,7 +28,7 @@ from legendary.utils.custom_parser import HiddenAliasSubparsersAction
|
|||
from legendary.utils.env import is_windows_mac_or_pyi
|
||||
from legendary.lfs.eos import add_registry_entries, query_registry_entries, remove_registry_entries
|
||||
from legendary.lfs.utils import validate_files, clean_filename
|
||||
from legendary.utils.selective_dl import get_sdl_data, LGDEvaluationContext
|
||||
from legendary.utils.selective_dl import get_sdl_data, LGDEvaluationContext, EXTRA_FUNCTIONS
|
||||
from legendary.lfs.wine_helpers import read_registry, get_shell_folders, case_insensitive_file_search
|
||||
|
||||
# todo custom formatter for cli logger (clean info, highlighted error/warning)
|
||||
|
|
@ -911,55 +912,59 @@ class LegendaryCLI:
|
|||
else:
|
||||
logger.info(f'Using existing repair file: {repair_file}')
|
||||
|
||||
# check if SDL should be disabled
|
||||
sdl_enabled = not args.install_tag
|
||||
config_tags = self.core.lgd.config.get(game.app_name, 'install_opts', fallback=None)
|
||||
config_disable_sdl = self.core.lgd.config.getboolean(game.app_name, 'disable_sdl', fallback=False)
|
||||
# remove config flag if SDL is reset
|
||||
if config_disable_sdl and args.reset_sdl and not args.disable_sdl:
|
||||
self.core.lgd.config.remove_option(game.app_name, 'disable_sdl')
|
||||
# if config flag is not yet set, set it and remove previous install tags
|
||||
elif not config_disable_sdl and args.disable_sdl:
|
||||
logger.info('Clearing install tags from config and disabling SDL for title.')
|
||||
if config_tags:
|
||||
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
|
||||
config_tags = None
|
||||
self.core.lgd.config.set(game.app_name, 'disable_sdl', 'true')
|
||||
sdl_enabled = False
|
||||
# just disable SDL, but keep config tags that have been manually specified
|
||||
elif config_disable_sdl or args.disable_sdl:
|
||||
sdl_enabled = False
|
||||
|
||||
if sdl_enabled:
|
||||
# FIXME: Consider UpgradePathLogic - it lets automatically select options in new manifests when corresponding option was selected with older version
|
||||
if not self.core.is_installed(game.app_name) or config_tags is None or args.reset_sdl:
|
||||
context = LGDEvaluationContext(self.core)
|
||||
sdl_data = get_sdl_data(self.core.lgd.egl_content_path, game.app_name, game.app_version(args.platform))
|
||||
if sdl_data:
|
||||
if args.skip_sdl:
|
||||
args.install_tag = []
|
||||
for entry in sdl_data['Data']:
|
||||
if entry.get('IsRequired', 'false').lower() == 'true':
|
||||
args.install_tag.extend(entry.get('Tags', []))
|
||||
else:
|
||||
args.install_tag = sdl_prompt(sdl_data, game.app_title, context)
|
||||
# self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(args.install_tag))
|
||||
else:
|
||||
logger.error(f'Unable to get SDL data for {game.app_name}')
|
||||
logger.info('Checking for install components')
|
||||
config_options = self.core.lgd.config.get(game.app_name, 'install_components', fallback=None)
|
||||
install_components = args.install_component or []
|
||||
sdl_data = get_sdl_data(self.core.lgd.egl_content_path, game.app_name, game.app_version(args.platform))
|
||||
context = LGDEvaluationContext(self.core)
|
||||
if not self.core.is_installed(game.app_name) or config_options is None or args.reset_sdl:
|
||||
if sdl_data:
|
||||
if not args.skip_sdl:
|
||||
install_components = sdl_prompt(sdl_data, game.app_title, context)
|
||||
else:
|
||||
args.install_tag = config_tags.split(',')
|
||||
elif args.install_tag and not game.is_dlc and not args.no_install:
|
||||
config_tags = ','.join(args.install_tag)
|
||||
logger.info(f'Saving install tags for "{game.app_name}" to config: {config_tags}')
|
||||
elif not game.is_dlc:
|
||||
if config_tags and args.reset_sdl:
|
||||
logger.info('Clearing install tags from config.')
|
||||
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
|
||||
elif config_tags:
|
||||
logger.info(f'Using install tags from config: {config_tags}')
|
||||
args.install_tag = config_tags.split(',')
|
||||
|
||||
logger.debug(f'Selected tags: {args.install_tag}')
|
||||
logger.error(f'Unable to get SDL data for {game.app_name}')
|
||||
else:
|
||||
install_components = config_options.split(',')
|
||||
context.selection = set(install_components)
|
||||
|
||||
install_tags = set()
|
||||
if sdl_data:
|
||||
for element in sdl_data['Data']:
|
||||
if element.get('IsRequired', 'false').lower() == 'true':
|
||||
install_tags.update(element.get('Tags', []))
|
||||
if element['UniqueId'] not in install_components:
|
||||
install_components.append(element['UniqueId'])
|
||||
continue
|
||||
if element.get('Invisible', 'false').lower() == 'true':
|
||||
tk = Tokenizer(element['InvisibleSelectedExpression'], context)
|
||||
tk.extend_functions(EXTRA_FUNCTIONS)
|
||||
tk.compile()
|
||||
if tk.execute(''):
|
||||
logger.info('Selecting invisible component')
|
||||
install_tags.update(element.get('Tags', []))
|
||||
if element['UniqueId'] not in install_components:
|
||||
install_components.append(element['UniqueId'])
|
||||
|
||||
# The ids may change from revision to revision, this property lets us match options against older options
|
||||
upgrade_id = element.get('UpgradePathLogic')
|
||||
if upgrade_id and upgrade_id in install_components:
|
||||
install_tags.update(element.get('Tags', []))
|
||||
# Replace component id with upgraded one
|
||||
install_components = [element['UniqueId'] if el == upgrade_id else el for el in install_components]
|
||||
|
||||
if element['UniqueId'] in install_components:
|
||||
install_tags.update(element.get('Tags', []))
|
||||
|
||||
if element.get('ConfigHandler'):
|
||||
for child in element.get('Children', []):
|
||||
if child['UniqueId'] in install_components:
|
||||
install_tags.update(child.get('Tags', []))
|
||||
|
||||
if install_components:
|
||||
self.core.lgd.config.set(game.app_name, 'install_components', ','.join(install_components))
|
||||
install_tags = list(install_tags)
|
||||
logger.debug(f'Selected components: {install_components}')
|
||||
logger.debug(f'Selected tags: {install_tags}')
|
||||
logger.info(f'Preparing download for "{game.app_title}" ({game.app_name})...')
|
||||
# todo use status queue to print progress from CLI
|
||||
# This has become a little ridiculous hasn't it?
|
||||
|
|
@ -974,7 +979,8 @@ class LegendaryCLI:
|
|||
file_prefix_filter=args.file_prefix,
|
||||
file_suffix_filter=args.file_suffix,
|
||||
file_exclude_filter=args.file_exclude_prefix,
|
||||
file_install_tag=args.install_tag,
|
||||
file_install_tag=install_tags,
|
||||
game_install_components=install_components,
|
||||
dl_optimizations=args.order_opt,
|
||||
dl_timeout=args.dl_timeout,
|
||||
repair=args.repair_mode,
|
||||
|
|
@ -1646,6 +1652,7 @@ class LegendaryCLI:
|
|||
f'not being available on the selected platform or currently logged-in account.')
|
||||
args.offline = True
|
||||
|
||||
sdl_data = get_sdl_data(self.core.lgd.egl_content_path, app_name, game.app_version(args.platform)) or {}
|
||||
manifest_data = None
|
||||
entitlements = None
|
||||
# load installed manifest or URI
|
||||
|
|
@ -1765,11 +1772,11 @@ class LegendaryCLI:
|
|||
igame.save_path))
|
||||
installation_info.append(InfoItem('EGL sync GUID', 'synced_egl_guid', igame.egl_guid,
|
||||
igame.egl_guid))
|
||||
if igame.install_tags:
|
||||
tags = ', '.join(igame.install_tags)
|
||||
if igame.install_components:
|
||||
opts = ', '.join(igame.install_components)
|
||||
else:
|
||||
tags = '(None, all game data selected for install)'
|
||||
installation_info.append(InfoItem('Install tags', 'install_tags', tags, igame.install_tags))
|
||||
opts = '(None, all game data selected for install)'
|
||||
installation_info.append(InfoItem('Install components', 'install_components', opts, igame.install_components))
|
||||
installation_info.append(InfoItem('Requires ownership verification token (DRM)', 'requires_ovt',
|
||||
igame.requires_ot, igame.requires_ot))
|
||||
|
||||
|
|
@ -1843,9 +1850,23 @@ class LegendaryCLI:
|
|||
for tag in fm.install_tags:
|
||||
install_tags.add(tag)
|
||||
|
||||
install_tags = sorted(install_tags)
|
||||
install_tags_human = ', '.join(i if i else '(empty)' for i in install_tags)
|
||||
manifest_info.append(InfoItem('Install tags', 'install_tags', install_tags_human, install_tags))
|
||||
if sdl_data:
|
||||
install_tags_human = []
|
||||
for element in sdl_data['Data']:
|
||||
if not element.get('Title'):
|
||||
continue
|
||||
is_required = element.get('IsRequired','false')=='true'
|
||||
required_txt = ' (required)' if is_required else ''
|
||||
if element.get('Children'):
|
||||
install_tags_human.append(f'{element["Title"]}{required_txt}')
|
||||
for child in element.get('Children', []):
|
||||
install_tags_human.append(f'\t{child["UniqueId"]} - {child["Title"]}')
|
||||
else:
|
||||
install_tags_human.append(f'{element["UniqueId"]} - {element["Title"]}{required_txt}')
|
||||
else:
|
||||
install_tags_human = '(none)'
|
||||
|
||||
manifest_info.append(InfoItem('Install components', 'install_components', install_tags_human, sdl_data.get('Data')))
|
||||
# file and chunk count
|
||||
manifest_info.append(InfoItem('Files', 'num_files', manifest.file_manifest_list.count,
|
||||
manifest.file_manifest_list.count))
|
||||
|
|
@ -1862,6 +1883,7 @@ class LegendaryCLI:
|
|||
chunk_size, total_size))
|
||||
|
||||
# if there are install tags break down size by tag
|
||||
"""
|
||||
tag_disk_size = []
|
||||
tag_disk_size_human = []
|
||||
tag_download_size = []
|
||||
|
|
@ -1896,6 +1918,7 @@ class LegendaryCLI:
|
|||
tag_disk_size_human or 'N/A', tag_disk_size))
|
||||
manifest_info.append(InfoItem('Download size by install tag', 'tag_download_size',
|
||||
tag_download_size_human or 'N/A', tag_download_size))
|
||||
"""
|
||||
|
||||
if not args.json:
|
||||
def print_info_item(item: InfoItem):
|
||||
|
|
@ -2772,8 +2795,8 @@ def main():
|
|||
help='Only fetch files whose path ends with <prefix> (case insensitive)')
|
||||
install_parser.add_argument('--exclude', dest='file_exclude_prefix', action='append', metavar='<prefix>',
|
||||
type=str, help='Exclude files starting with <prefix> (case insensitive)')
|
||||
install_parser.add_argument('--install-tag', dest='install_tag', action='append', metavar='<tag>',
|
||||
type=str, help='Only download files with the specified install tag')
|
||||
install_parser.add_argument('--install-component', dest='install_component', action='append', metavar='<id>',
|
||||
type=str, help='Only download files with the specified optional download id')
|
||||
install_parser.add_argument('--enable-reordering', dest='order_opt', action='store_true',
|
||||
help='Enable reordering optimization to reduce RAM requirements '
|
||||
'during download (may have adverse results for some titles)')
|
||||
|
|
@ -2902,8 +2925,8 @@ def main():
|
|||
list_files_parser.add_argument('--json', dest='json', action='store_true', help='Output in JSON format')
|
||||
list_files_parser.add_argument('--hashlist', dest='hashlist', action='store_true',
|
||||
help='Output file hash list in hashcheck/sha1sum -c compatible format')
|
||||
list_files_parser.add_argument('--install-tag', dest='install_tag', action='store', metavar='<tag>',
|
||||
type=str, help='Show only files with specified install tag')
|
||||
list_files_parser.add_argument('--install-component', dest='install_component', action='store', metavar='<id>',
|
||||
type=str, help='Show only files with specified optional download id')
|
||||
|
||||
sync_saves_parser.add_argument('--skip-upload', dest='download_only', action='store_true',
|
||||
help='Only download new saves from cloud, don\'t upload')
|
||||
|
|
|
|||
|
|
@ -1349,7 +1349,7 @@ class LegendaryCore:
|
|||
repair_use_latest: bool = False, disable_delta: bool = False,
|
||||
override_delta_manifest: str = '', egl_guid: str = '',
|
||||
preferred_cdn: str = None, disable_https: bool = False,
|
||||
bind_ip: str = None) -> (DLManager, AnalysisResult, ManifestMeta):
|
||||
bind_ip: str = None, game_install_components: list = None) -> (DLManager, AnalysisResult, ManifestMeta):
|
||||
# load old manifest
|
||||
old_manifest = None
|
||||
|
||||
|
|
@ -1558,6 +1558,7 @@ class LegendaryCore:
|
|||
can_run_offline=offline == 'true', requires_ot=ot == 'true',
|
||||
is_dlc=base_game is not None, install_size=anlres.install_size,
|
||||
egl_guid=egl_guid, install_tags=file_install_tag,
|
||||
install_components=game_install_components,
|
||||
platform=platform, uninstaller=uninstaller)
|
||||
|
||||
return dlm, anlres, igame
|
||||
|
|
@ -1871,9 +1872,10 @@ class LegendaryCore:
|
|||
version=new_manifest.meta.build_version,
|
||||
platform='Windows')
|
||||
|
||||
# TODO: Migrate this to components instead?
|
||||
# transfer install tag choices to config
|
||||
if lgd_igame.install_tags:
|
||||
self.lgd.config.set(app_name, 'install_tags', ','.join(lgd_igame.install_tags))
|
||||
#if lgd_igame.install_tags:
|
||||
# self.lgd.config.set(app_name, 'install_tags', ','.join(lgd_igame.install_tags))
|
||||
|
||||
# mark game as installed
|
||||
_ = self._install_game(lgd_igame)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class EGLManifest:
|
|||
self.install_location = None
|
||||
self.install_size = None
|
||||
self.install_tags = None
|
||||
self.install_components = None
|
||||
self.installation_guid = None
|
||||
self.launch_command = None
|
||||
self.executable = None
|
||||
|
|
@ -87,6 +88,7 @@ class EGLManifest:
|
|||
tmp.install_location = json.pop('InstallLocation', '')
|
||||
tmp.install_size = json.pop('InstallSize', 0)
|
||||
tmp.install_tags = json.pop('InstallTags', [])
|
||||
tmp.install_components = json.pop('InstallComponents', [])
|
||||
tmp.installation_guid = json.pop('InstallationGuid', '')
|
||||
tmp.launch_command = json.pop('LaunchCommand', '')
|
||||
tmp.executable = json.pop('LaunchExecutable', '')
|
||||
|
|
@ -114,6 +116,7 @@ class EGLManifest:
|
|||
out['InstallLocation'] = self.install_location
|
||||
out['InstallSize'] = self.install_size
|
||||
out['InstallTags'] = self.install_tags
|
||||
out['InstallComponents'] = self.install_components
|
||||
out['InstallationGuid'] = self.installation_guid
|
||||
out['LaunchCommand'] = self.launch_command
|
||||
out['LaunchExecutable'] = self.executable
|
||||
|
|
@ -140,6 +143,7 @@ class EGLManifest:
|
|||
tmp.install_location = igame.install_path
|
||||
tmp.install_size = igame.install_size
|
||||
tmp.install_tags = igame.install_tags
|
||||
tmp.install_components = igame.install_components
|
||||
tmp.installation_guid = igame.egl_guid
|
||||
tmp.launch_command = igame.launch_parameters
|
||||
tmp.executable = igame.executable
|
||||
|
|
@ -159,4 +163,4 @@ class EGLManifest:
|
|||
launch_parameters=self.launch_command, can_run_offline=self.can_run_offline,
|
||||
requires_ot=self.ownership_token, is_dlc=False,
|
||||
needs_verification=self.needs_validation, install_size=self.install_size,
|
||||
egl_guid=self.installation_guid, install_tags=self.install_tags)
|
||||
egl_guid=self.installation_guid, install_tags=self.install_tags, install_components=self.install_components)
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ class InstalledGame:
|
|||
executable: str = ''
|
||||
install_size: int = 0
|
||||
install_tags: List[str] = field(default_factory=list)
|
||||
install_components: List[str] = field(default_factory=list)
|
||||
is_dlc: bool = False
|
||||
launch_parameters: str = ''
|
||||
manifest_path: str = ''
|
||||
|
|
@ -218,6 +219,7 @@ class InstalledGame:
|
|||
tmp.install_size = json.get('install_size', 0)
|
||||
tmp.egl_guid = json.get('egl_guid', '')
|
||||
tmp.install_tags = json.get('install_tags', [])
|
||||
tmp.install_components = json.get('install_components', [])
|
||||
return tmp
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,6 @@ def get_int_choice(prompt, default=None, min_choice=None, max_choice=None, retur
|
|||
|
||||
|
||||
def sdl_prompt(sdl_data, title, context):
|
||||
tags = set()
|
||||
|
||||
print(f'You are about to install {title}, this application supports selective downloads.')
|
||||
choices = []
|
||||
required_categories = {}
|
||||
|
|
@ -86,27 +84,7 @@ def sdl_prompt(sdl_data, title, context):
|
|||
choices=choices,
|
||||
cycle=True,
|
||||
validate=lambda selected: not required_categories or all(any(item in selected for item in category) for category in required_categories.values())).execute()
|
||||
context.selection = set(selected_packs)
|
||||
|
||||
for element in sdl_data['Data']:
|
||||
if element.get('IsRequired', 'false').lower() == 'true':
|
||||
tags.update(element.get('Tags', []))
|
||||
continue
|
||||
if element.get('Invisible', 'false').lower() == 'true':
|
||||
tk = Tokenizer(element['InvisibleSelectedExpression'], context)
|
||||
tk.extend_functions(EXTRA_FUNCTIONS)
|
||||
tk.compile()
|
||||
if tk.execute(''):
|
||||
tags.update(element.get('Tags', []))
|
||||
|
||||
if element['UniqueId'] in selected_packs:
|
||||
tags.update(element.get('Tags', []))
|
||||
if element.get('ConfigHandler'):
|
||||
for child in element.get('Children', []):
|
||||
if child['UniqueId'] in selected_packs:
|
||||
tags.update(child.get('Tags', []))
|
||||
|
||||
return list(tags)
|
||||
return selected_packs
|
||||
|
||||
|
||||
def strtobool(val):
|
||||
|
|
|
|||
|
|
@ -44,4 +44,4 @@ def get_sdl_data(location, app_name, app_version):
|
|||
|
||||
if applying_meta:
|
||||
return applying_meta[-1]
|
||||
return None
|
||||
return None
|
||||
|
|
|
|||
Loading…
Reference in a new issue