[cli/core/downloader] Support filtering by install tags

Also adds tsv option for list-files and fixes
no_install not being set with --exclude.

Install tags are only present in some titles, I'm not
entirely sure how EGL uses them. Perhaps to allow one
manifest to be used on different platforms? Or to only
download extra assets when the user wants to?

Either way, it's another filtering feature that may be
useful, though for now it's mostly another toy to explore
EPIC's distribution system with.
This commit is contained in:
derrod 2020-05-01 14:34:34 +02:00
parent 531af3f586
commit 9b5620ca30
3 changed files with 36 additions and 10 deletions

View file

@ -173,16 +173,24 @@ class LegendaryCLI:
files = sorted(manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower())
if args.install_tag:
files = [fm for fm in files if args.install_tag in fm.install_tags]
if args.hashlist:
for fm in files:
print(f'{fm.hash.hex()} *{fm.filename}')
elif args.csv:
writer = csv.writer(stdout)
writer.writerow(['path', 'hash', 'size'])
writer.writerows((fm.filename, fm.hash.hex(), fm.file_size) for fm in files)
elif args.csv or args.tsv:
writer = csv.writer(stdout, dialect='excel-tab' if args.tsv else 'excel')
writer.writerow(['path', 'hash', 'size', 'install_tags'])
writer.writerows((fm.filename, fm.hash.hex(), fm.file_size, '|'.join(fm.install_tags))for fm in files)
else:
install_tags = set()
for fm in files:
print(fm.filename)
for t in fm.install_tags:
install_tags.add(t)
# use the log output so this isn't included when piping file list into file
logger.info(f'Install tags: {", ".join(sorted(install_tags))}')
def launch_game(self, args, extra):
app_name = args.app_name
@ -232,7 +240,7 @@ class LegendaryCLI:
logger.error('Login failed! Cannot continue with download process.')
exit(1)
if args.file_prefix:
if args.file_prefix or args.file_exclude_prefix or args.install_tag:
args.no_install = True
if args.update_only:
@ -273,7 +281,8 @@ class LegendaryCLI:
override_base_url=args.override_base_url,
platform_override=args.platform_override,
file_prefix_filter=args.file_prefix,
file_exclude_filter=args.file_exclude_prefix)
file_exclude_filter=args.file_exclude_prefix,
file_install_tag=args.install_tag)
# game is either up to date or hasn't changed, so we have nothing to do
if not analysis.dl_size:
@ -454,6 +463,8 @@ def main():
help='Only fetch files whose path starts with <prefix> (case insensitive)')
install_parser.add_argument('--exclude', dest='file_exclude_prefix', action='store', metavar='<prefix>',
type=str, help='Exclude files starting with <prefix> (case insensitive)')
install_parser.add_argument('--install-tag', dest='install_tag', action='store', metavar='<prefix>',
type=str, help='Only download files with the specified install tag (testing)')
launch_parser.add_argument('--offline', dest='offline', action='store_true',
default=False, help='Skip login and launch game without online authentication')
@ -485,8 +496,11 @@ def main():
list_files_parser.add_argument('--manifest', dest='override_manifest', action='store', metavar='<uri>',
help='Manifest URL or path to use instead of the CDN one')
list_files_parser.add_argument('--csv', dest='csv', action='store_true', help='Output in CSV format')
list_files_parser.add_argument('--tsv', dest='tsv', action='store_true', help='Output in TSV format')
list_files_parser.add_argument('--hashlist', dest='hashlist', action='store_true',
help='Output file hash list in hashcheck/sha1sum compatible format')
list_files_parser.add_argument('--install-tag', dest='install_tag', action='store', metavar='<prefix>',
type=str, help='Show only files with specified install tag')
args, extra = parser.parse_known_args()

View file

@ -356,7 +356,8 @@ class LegendaryCore:
game_folder: str = '', override_manifest: str = '',
override_old_manifest: str = '', override_base_url: str = '',
platform_override: str = '', file_prefix_filter: str = '',
file_exclude_filter: str = '' ) -> (DLManager, AnalysisResult, ManifestMeta):
file_exclude_filter: str = '', file_install_tag: str = ''
) -> (DLManager, AnalysisResult, ManifestMeta):
# load old manifest
old_manifest = None
@ -430,7 +431,8 @@ class LegendaryCore:
anlres = dlm.run_analysis(manifest=new_manifest, old_manifest=old_manifest,
patch=not disable_patching, resume=not force,
file_prefix_filter=file_prefix_filter,
file_exclude_filter=file_exclude_filter)
file_exclude_filter=file_exclude_filter,
file_install_tag=file_install_tag)
prereq = None
if new_manifest.meta.prereq_ids:

View file

@ -242,7 +242,7 @@ class DLManager(Process):
def run_analysis(self, manifest: Manifest, old_manifest: Manifest = None,
patch=True, resume=True, file_prefix_filter=None,
file_exclude_filter=None) -> AnalysisResult:
file_exclude_filter=None, file_install_tag=None) -> AnalysisResult:
"""
Run analysis on manifest and old manifest (if not None) and return a result
with a summary resources required in order to install the provided manifest.
@ -278,6 +278,16 @@ class DLManager(Process):
except Exception as e:
self.log.warning(f'Reading resume file failed: {e!r}, continuing as normal...')
# Not entirely sure what install tags are used for, only some titles have them.
# Let's add it for testing anyway.
if file_install_tag:
files_to_skip = set(i.filename for i in manifest.file_manifest_list.elements
if file_install_tag not in i.install_tags)
self.log.info(f'Found {len(files_to_skip)} files to skip based on install tag.')
mc.added -= files_to_skip
mc.changed -= files_to_skip
mc.unchanged |= files_to_skip
# if include/exclude prefix has been set: mark all files that are not to be downloaded as unchanged
if file_exclude_filter:
file_exclude_filter = file_exclude_filter.lower()
@ -295,7 +305,7 @@ class DLManager(Process):
mc.changed -= files_to_skip
mc.unchanged |= files_to_skip
if file_prefix_filter or file_exclude_filter:
if file_prefix_filter or file_exclude_filter or file_install_tag:
self.log.info(f'Remaining files after filtering: {len(mc.added) + len(mc.changed)}')
# correct install size after filtering
analysis_res.install_size = sum(fm.file_size for fm in manifest.file_manifest_list.elements