def test_choose_full_path(systemize): app_name = "Anna's Quest" executables = [ PureWindowsPath('C:\\Games\\AnnasQuest\\uninst.exe'), PureWindowsPath('C:\\Games\\AnnasQuest\\anna.exe') ] executables = systemize(executables) expected = executables[1] res = PathFinder.choose_main_executable(app_name, executables) assert res == str(expected)
def test_choose_legendary_heroes(systemize): app_name = "Fallen Enchantress: Legendary Heroes" executables = [ PureWindowsPath('C:\\Users\\me\\humblebundle\\DataZip.exe'), PureWindowsPath('C:\\Users\\me\\humblebundle\\DXAtlasWin.exe'), PureWindowsPath('C:\\Users\\me\\humblebundle\\LegendaryHeroes.exe'), PureWindowsPath('C:\\Users\\me\\humblebundle\\LH_prefs_setup.exe') ] executables = systemize(executables) expected = executables[2] res = PathFinder.choose_main_executable(app_name, executables) assert res == expected
def __init__(self, get_close_matches=None, find_best_exe=None): self._pathfinder = PathFinder(CURRENT_SYSTEM) self.get_close_matches = get_close_matches or self._get_close_matches self.find_best_exe = find_best_exe or self._find_best_exe
class BaseAppFinder(abc.ABC): def __init__(self, get_close_matches=None, find_best_exe=None): self._pathfinder = PathFinder(CURRENT_SYSTEM) self.get_close_matches = get_close_matches or self._get_close_matches self.find_best_exe = find_best_exe or self._find_best_exe async def __call__(self, owned_title_id: Dict[str, str], paths: Set[pathlib.Path]) -> Dict[str, LocalHumbleGame]: """ :param owned_title_id: human_name: machine_name dictionary """ start = time.time() found_games = await self._scan_folders(paths, set(owned_title_id)) local_games = { owned_title_id[title]: LocalHumbleGame(owned_title_id[title], exe) for title, exe in found_games.items() } logging.debug(f'=== Scanning folders took {time.time() - start}') return local_games async def _scan_folders(self, paths: Iterable[Union[str, os.PathLike]], app_names: Set[str]) -> Dict[str, pathlib.Path]: """ :param paths: all master paths to be scan for app finding :param app_names: app names to be matched with folder names :returns: mapping of app names to found executables """ not_yet_found: Set[str] = app_names.copy() result: Dict[str, pathlib.Path] = {} close_matches: Dict[str, pathlib.Path] = {} # exact matches for path in paths: async for app_name, exe in self.__scan(path, not_yet_found, similarity=1): result[app_name] = exe # close matches for path in paths: async for app_name, exe in self.__scan(path, not_yet_found, similarity=0.8): close_matches[app_name] = exe # overwrite close matches with exact results close_matches.update(result) return close_matches async def __scan( self, path: Union[str, os.PathLike], candidates: Set[str], similarity: float ) -> AsyncGenerator[Tuple[str, pathlib.Path], None]: """One level depth search generator for application execs based on similarity with candidate names. :param path: root dir of which subdirectories will be scanned :param candidates: set of app names used for exact and close matching with directory names :param similarity: cutoff level for difflib.get_close_matches; set 1 for exact matching only :yields: 2-el. tuple of app_name and executable """ root, dirs, _ = next(os.walk(path)) logging.debug( f'New scan - similarity: {similarity}, candidates: {list(candidates)}' ) for dir_name in dirs: await asyncio.sleep(0) matches = self.get_close_matches(dir_name, candidates, similarity) for app_name in matches: dir_path = pathlib.PurePath(root) / dir_name best_exe = self.find_best_exe(dir_path, app_name) if best_exe is None: logging.warning( 'No executable found, moving to next best matched app') continue candidates.remove(app_name) yield app_name, pathlib.Path(best_exe) break def _get_close_matches(self, dir_name: str, candidates: Set[str], similarity: float) -> List[str]: """Wrapper around difflib.get_close_matches""" matches_ = difflib.get_close_matches(dir_name, candidates, cutoff=similarity) matches = cast(List[str], matches_) # as str is Sequence[str] - mypy/issues/5090 if matches: logging.info( f'found close ({similarity}) matches for {dir_name}: {matches}' ) return matches def _find_best_exe(self, dir_path: pathlib.PurePath, app_name: str): executables = self._pathfinder.find_executables(dir_path) if not executables: return None logging.debug(f'Found execs: {executables}') return self._pathfinder.choose_main_executable(app_name, executables)
def __init__(self, get_close_matches=None, find_best_exe=None): self._pathfinder = PathFinder(IS_WINDOWS) self.get_close_matches = get_close_matches or self._get_close_matches self.find_best_exe = find_best_exe or self._find_best_exe
def __init__(self): self._reg = WinRegUninstallWatcher(ignore_filter=self.is_other_store_game) self._pathfinder = PathFinder(HP.WINDOWS)
class WindowsAppFinder: def __init__(self): self._reg = WinRegUninstallWatcher(ignore_filter=self.is_other_store_game) self._pathfinder = PathFinder(HP.WINDOWS) @staticmethod def is_other_store_game(key_name) -> bool: """Exclude Steam and GOG games using uninstall key name. In the future probably more DRM-free stores should be supported """ match = re.match(r'\d{10}_is1', key_name) # GOG.com if match: return True return "Steam App" in key_name @staticmethod def _matches(human_name: str, uk: UninstallKey) -> bool: def escape(x): return x.replace(':', '').lower() def escaped_matches(a, b): return escape(a) == escape(b) def norm(x): return x.replace(" III", " 3").replace(" II", " 2") if human_name == uk.display_name \ or escaped_matches(human_name, uk.display_name) \ or uk.key_name.lower().startswith(human_name.lower()): return True location = uk.install_location_path if location: if escaped_matches(human_name, location.name): return True else: location = uk.uninstall_string_path or uk.display_icon_path if location: if escaped_matches(human_name, location.parent.name): return True # quickfix for Torchlight II ect., until better solution will be provided return escaped_matches(norm(human_name), norm(uk.display_name)) def _find_executable(self, human_name: str, uk: UninstallKey) -> Optional[pathlib.Path]: """ Returns most probable app executable of given uk or None if not found. """ # sometimes display_icon link to main executable upath = uk.uninstall_string_path ipath = uk.display_icon_path if ipath and ipath.suffix == '.exe': if ipath != upath and 'unins' not in str(ipath): # exclude uninstaller return ipath # get install_location if present; if not, check for uninstall or display_icon parents location = uk.install_location_path \ or (upath.parent if upath else None) \ or (ipath.parent if ipath else None) # find all executables and get best machting (exclude uninstall_path) if location and location.exists(): executables = set(self._pathfinder.find_executables(location)) - {str(upath)} best_match = self._pathfinder.choose_main_executable(human_name, executables) if best_match is None: logging.warning(f'Main exe not found for {human_name}; \ loc: {uk.install_location}; up: {upath}; ip: {ipath}; execs: {executables}') return None return pathlib.Path(best_match) return None async def find_local_games(self, owned_games: List[HumbleGame]) -> List[LocalHumbleGame]: local_games = [] while self._reg.uninstall_keys: uk = self._reg.uninstall_keys.pop() try: for og in owned_games: if isinstance(og, Key): continue if self._matches(og.human_name, uk): exe = self._find_executable(og.human_name, uk) if exe is not None: game = LocalHumbleGame(og.machine_name, exe, uk.uninstall_string) logging.info(f'New local game found: {game}') local_games.append(game) break logging.warning(f"Uninstall key matched, but cannot find \ game exe for [{og.human_name}]; uk: {uk}") except Exception: self._reg.uninstall_keys.add(uk) raise await asyncio.sleep(0.001) # makes this method non blocking return local_games def refresh(self): self._reg.refresh()
def test_choose_2exe(): app_name = "Anna's Quest" executables = ['anna.exe', 'uninst.exe'] res = PathFinder.choose_main_executable(app_name, executables) assert res == 'anna.exe'
def test_choose_icase(): app_name = "LIMBO" executables = ['limbo.exe', 'other.exe', 'unst000.exe', 'LIM-editor.exe'] res = PathFinder.choose_main_executable(app_name, executables) assert res == 'limbo.exe'
def test_choose_exact_match(): app_name = "The Game" executables = ['the game.exe', 'map_editor.exe'] res = PathFinder.choose_main_executable(app_name, executables) assert res == 'the game.exe'
def test_choose_1exe(): app_name = "Wildcharts" executables = ['game.exe'] res = PathFinder.choose_main_executable(app_name, executables) assert res == executables[0]