[cli/core/utils/lfs] Add automatic alias generation

This commit is contained in:
derrod 2021-10-02 21:10:25 +02:00
parent d70d5a6521
commit 0d8b74a9e0
4 changed files with 155 additions and 2 deletions

View file

@ -51,9 +51,15 @@ class LegendaryCLI:
ql.start() ql.start()
return ql return ql
def _resolve_aliases(self, app_name): def _resolve_aliases(self, name):
# make sure aliases exist if not yet created
self.core.update_aliases(force=False)
name = name.strip()
# resolve alias (if any) to real app name # resolve alias (if any) to real app name
return self.core.lgd.config.get('Legendary.aliases', app_name, fallback=app_name) return self.core.lgd.config.get(
section='Legendary.aliases', option=name,
fallback=self.core.lgd.aliases.get(name.lower(), name)
)
def auth(self, args): def auth(self, args):
if args.auth_delete: if args.auth_delete:

View file

@ -273,6 +273,11 @@ class LegendaryCore:
# return data if available # return data if available
return sdl_data return sdl_data
def update_aliases(self, force=False):
_aliases_enabled = not self.lgd.config.getboolean('Legendary', 'disable_auto_aliasing', fallback=False)
if _aliases_enabled and (force or not self.lgd.aliases):
self.lgd.generate_aliases()
def get_assets(self, update_assets=False, platform_override=None) -> List[GameAsset]: def get_assets(self, update_assets=False, platform_override=None) -> List[GameAsset]:
# do not save and always fetch list when platform is overridden # do not save and always fetch list when platform is overridden
if platform_override: if platform_override:
@ -311,6 +316,7 @@ class LegendaryCore:
force_refresh=False, skip_ue=True) -> (List[Game], Dict[str, List[Game]]): force_refresh=False, skip_ue=True) -> (List[Game], Dict[str, List[Game]]):
_ret = [] _ret = []
_dlc = defaultdict(list) _dlc = defaultdict(list)
meta_updated = False
for ga in self.get_assets(update_assets=update_assets, for ga in self.get_assets(update_assets=update_assets,
platform_override=platform_override): platform_override=platform_override):
@ -328,6 +334,7 @@ class LegendaryCore:
app_title=eg_meta['title'], asset_info=ga, metadata=eg_meta) app_title=eg_meta['title'], asset_info=ga, metadata=eg_meta)
if not platform_override: if not platform_override:
meta_updated = True
self.lgd.set_game_meta(game.app_name, game) self.lgd.set_game_meta(game.app_name, game)
# replace asset info with the platform specific one if override is used # replace asset info with the platform specific one if override is used
@ -340,6 +347,9 @@ class LegendaryCore:
elif not any(i['path'] == 'mods' for i in game.metadata.get('categories', [])): elif not any(i['path'] == 'mods' for i in game.metadata.get('categories', [])):
_ret.append(game) _ret.append(game)
if not platform_override:
self.update_aliases(force=meta_updated)
return _ret, _dlc return _ret, _dlc
def get_non_asset_library_items(self, force_refresh=False, def get_non_asset_library_items(self, force_refresh=False,

View file

@ -4,10 +4,12 @@ import json
import os import os
import logging import logging
from collections import defaultdict
from pathlib import Path from pathlib import Path
from time import time from time import time
from legendary.models.game import * from legendary.models.game import *
from legendary.utils.aliasing import generate_aliases
from legendary.utils.config import LGDConf from legendary.utils.config import LGDConf
from legendary.utils.env import is_windows_or_pyi from legendary.utils.env import is_windows_or_pyi
from legendary.utils.lfs import clean_filename from legendary.utils.lfs import clean_filename
@ -112,6 +114,17 @@ class LGDLFS:
except Exception as e: except Exception as e:
self.log.debug(f'Loading game meta file "{gm_file}" failed: {e!r}') self.log.debug(f'Loading game meta file "{gm_file}" failed: {e!r}')
# load auto-aliases if enabled
self.aliases = dict()
if not self.config.getboolean('Legendary', 'disable_auto_aliasing', fallback=False):
try:
_j = json.load(open(os.path.join(self.path, 'aliases.json')))
for app_name, aliases in _j.items():
for alias in aliases:
self.aliases[alias] = app_name
except Exception as e:
self.log.debug(f'Loading aliases failed with {e!r}')
@property @property
def userdata(self): def userdata(self):
if self._user_data is not None: if self._user_data is not None:
@ -339,3 +352,38 @@ class LGDLFS:
json.dump(dict(version=sdl_version, data=sdl_data), json.dump(dict(version=sdl_version, data=sdl_data),
open(os.path.join(self.path, 'tmp', f'{app_name}.json'), 'w'), open(os.path.join(self.path, 'tmp', f'{app_name}.json'), 'w'),
indent=2, sort_keys=True) indent=2, sort_keys=True)
def generate_aliases(self):
self.log.debug('Generating list of aliases...')
aliases = set()
collisions = set()
alias_map = defaultdict(set)
for app_name in self._game_metadata.keys():
game = self.get_game_meta(app_name)
if game.is_dlc:
continue
game_folder = game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', None)
_aliases = generate_aliases(game.app_title, game_folder)
for alias in _aliases:
if alias not in aliases:
aliases.add(alias)
alias_map[game.app_name].add(alias)
else:
collisions.add(alias)
# remove colliding aliases from map and add aliases to lookup table
for app_name, aliases in alias_map.items():
alias_map[app_name] -= collisions
for alias in alias_map[app_name]:
self.aliases[alias] = app_name
def serialise_sets(obj):
"""Turn sets into sorted lists for storage"""
if isinstance(obj, set):
return sorted(obj)
return obj
json.dump(alias_map, open(os.path.join(self.path, 'aliases.json'), 'w', newline='\n'),
indent=2, sort_keys=True, default=serialise_sets)

View file

@ -0,0 +1,89 @@
from string import ascii_lowercase, digits
# Aliases generated:
# - name lowercase (without TM etc.)
# - same, but without spaces
# - same, but roman numerals are replaced
# if name has >= 3 parts:
# - initials
# - initials, but roman numerals are intact
# - initials, but roman numerals are replaced with number
# if ':' in name:
# - run previous recursively with everything before ":"
# if single 'f' in long word:
# - split word (this is mainly for cases like Battlfront -> BF)
allowed_characters = ascii_lowercase+digits
roman = {
# 'i': '1',
'ii': '2',
'iii': '3',
'iv': '4',
'v': '5',
'vi': '6',
'vii': '7',
'viii': '8',
'ix': '9',
'x': '10',
'xi': '11',
'xii': '12',
'xiii': '13',
'xiv': '14',
'xv': '15',
'xvi': '16',
'xvii': '17',
'xviii': '18',
'xix': '19',
'xx': '20'
}
def _filter(input):
return ''.join(l for l in input if l in allowed_characters)
def generate_aliases(game_name, game_folder=None, split_words=True):
# normalise and split name, then filter for legal characters
game_parts = [_filter(p) for p in game_name.lower().split()]
# filter out empty parts
game_parts = [p for p in game_parts if p]
_aliases = [
' '.join(game_parts),
''.join(game_parts),
''.join(roman.get(p, p) for p in game_parts),
]
# single word abbreviation
first_word = next(i for i in game_parts if i not in ('for', 'the'))
if len(first_word) > 1:
_aliases.append(first_word)
# remove subtitle from game
if ':' in game_name:
_aliases.extend(generate_aliases(game_name.partition(':')[0]))
# include folder name for alternative short forms
if game_folder:
_aliases.extend(generate_aliases(game_folder, split_words=False))
# include initialisms
if len(game_parts) > 2:
_aliases.append(''.join(p[0] for p in game_parts))
_aliases.append(''.join(p[0] if p not in roman else p for p in game_parts))
_aliases.append(''.join(roman.get(p, p[0]) for p in game_parts))
# Attempt to address cases like "Battlefront" being shortened to "BF"
if split_words:
new_game_parts = []
for word in game_parts:
if len(word) >= 8 and word[3:-3].count('f') == 1:
word_middle = word[3:-3]
word_split = ' f'.join(word_middle.split('f'))
word = word[0:3] + word_split + word[-3:]
new_game_parts.extend(word.split())
else:
new_game_parts.append(word)
_aliases.append(''.join(p[0] for p in new_game_parts))
_aliases.append(''.join(p[0] if p not in roman else p for p in new_game_parts))
_aliases.append(''.join(roman.get(p, p[0]) for p in new_game_parts))
# return sorted unqiues
return sorted(set(_aliases))