def load(self): self.mods_json = JSONFile( os.path.join( self.config.mods_directory, 'mod-list.json' ) ) if 'mods' not in self.mods_json: self.mods_json.mods = []
def __init__(self, config, api): self.api = api self.config = config self.mods_json = JSONFile( os.path.join( self.config.mods_path, 'mod-list.json' ) )
def __init__(self, config, api): self.config = config self.api = api self.cache_dir = user_cache_dir('fac', appauthor=False) self.storage = FileStorage(os.path.join(self.cache_dir, 'index')) self.schema = Schema( name=TEXT(sortable=True, phrase=True, field_boost=3, analyzer=intraword), owner=TEXT(sortable=True, field_boost=2.5, analyzer=intraword), title=TEXT(field_boost=2.0, phrase=False), summary=TEXT(phrase=True), downloads=NUMERIC(sortable=True), sort_name=SortColumn(), name_id=ID(stored=True), ) try: self.index = self.storage.open_index() except EmptyIndexError: self.index = None self.db = JSONFile(os.path.join(self.cache_dir, 'mods.json'))
class ModManager: """Provides access to the factorio mods directory""" def __init__(self, config, api, db): self.api = api self.config = config self.db = db self.mods_json = None def load(self): self.mods_json = JSONFile( os.path.join(self.config.mods_directory, 'mod-list.json')) if 'mods' not in self.mods_json: self.mods_json.mods = [] def get_mod_json(self, name): """Return the mod json configuration from mods-list.json""" for mod in self.mods_json.mods: if mod.name == name: return mod def get_mod(self, name, *args, **kwargs): for mod in self.find_mods(name, *args, **kwargs): if mod.name == name: return mod def find_mods(self, name=None, version=None, packed=None): mods = [] for mod_type in (ZippedMod, UnpackedMod): if packed is not None and mod_type.packed != bool(packed): continue mods.extend(mod_type.find(self, name, version)) return mods def resolve_mod_name(self, name, remote=False, patterns=True): if patterns and '*' in name: # Keep patterns unmodified return name # Find an exact match mod_names = set(mod.name for mod in self.find_mods()) if remote: mod_names |= set(self.db.mods) if name in mod_names: return name # Find a case-insensitive match for mod_name in mod_names: if mod_name.lower() == name.lower(): return mod_name # Find a unique partial match (case-insensitive) partial_matches = [ mod_name for mod_name in mod_names if name.lower() in mod_name.lower() ] if len(partial_matches) == 1: return partial_matches[0] if remote: # Find a remote match (case-insensitive) remote_mods = list(self.db.search(name, limit=5)) # If there was only one result, we can assume it's the one if len(remote_mods) == 1: return remote_mods[0].name elif len(remote_mods) > 1: print("'%s' not found, try one of the following:" % name) for match in remote_mods: print(" - " + match.name) print() raise ModNotFoundError(name) # If nothing was found, return original mod name and let things fail return name def resolve_remote_requirement(self, req, ignore_game_ver=False): spec = req.specifier game_ver = None if ignore_game_ver else self.config.game_version_major releases = self.get_releases(req.name, game_ver) yield from (release for release in releases if release.version in spec) def resolve_local_requirement(self, req, ignore_game_ver=False): spec = req.specifier game_ver = self.config.game_version_major res = [ mod for mod in self.find_mods(req.name) if mod.version in spec and ( ignore_game_ver or mod.game_version == game_ver) ] res.sort(key=lambda m: m.version, reverse=True) return res def get_releases(self, mod_name, game_version): try: mod = getattr(self.db.mods, mod_name) except AttributeError: raise ModNotFoundError(mod_name) latest = None if match_game_version(mod.latest_release, game_version): latest = mod.latest_release yield latest mod = self.api.get_mod(mod_name) res = [ release for release in mod.releases if match_game_version(release, game_version) and ( latest and release.version != latest.version) ] res.sort(key=lambda r: Version(r.version), reverse=True) yield from res def is_mod_enabled(self, name): mod = self.get_mod_json(name) if mod: # Factorio < 0.15 uses "true"/"false" strings instead of booleans return mod.enabled != 'false' and mod.enabled is not False else: return True # by default, new mods are automatically enabled def set_mod_enabled(self, name, enabled=True): mod = self.get_mod_json(name) if not mod: mod = {'enabled': '', 'name': name} self.mods_json.mods.append(mod) mod = self.get_mod_json(name) if enabled != self.is_mod_enabled(name): if self.config.game_version < Version('0.15'): # Factorio < 0.15 uses "true"/"false" strings # instead of booleans mod.enabled = 'true' if enabled else 'false' else: mod.enabled = enabled self.mods_json.save() return True else: return False def is_mod_held(self, name): return name in self.config.hold def set_mod_held(self, name, held=True): if self.is_mod_held(name) == held: return False if held: self.config.hold.append(name) else: self.config.hold.remove(name) self.config.save() return True def require_login(self, reset=False): import getpass import sys player_data = self.config.player_data username = player_data.get('service-username') token = player_data.get('service-token') if reset or not (username and token): print("You need a Factorio account to download mods.") print("Please provide your username and password to authenticate " "yourself.") print("Your username and token (NOT your password) will be stored " "so that you only have to enter it once") print("This uses the exact same method used by Factorio itself") print() while True: if username: print("Username [%s]:" % username, end=" ", flush=True) else: print("Username:"******" ", flush=True) input_username = sys.stdin.readline().strip() if input_username: username = input_username elif not username: continue password = getpass.getpass("Password (not shown):") if not password: continue try: token = self.api.login(username, password, require_ownership=True) except OwnershipError as ex: print("Ownership error: Your factorio account doesn't " "own the game.") print("Please buy the game or link your Steam account if " "you have bought the game from Steam.") except AuthError as ex: print("Authentication error: %s." % ex) except Exception as ex: print("Error: %s." % ex) else: print("Logged in successfully.") break print() player_data['service-token'] = token player_data['service-username'] = username player_data.save() return player_data def install_mod(self, mod_name, release, enable=None, unpack=None): file_name = release.file_name self.validate_mod_file_name(file_name) file_path = os.path.join(self.config.mods_directory, file_name) installed_mod = self.get_mod(mod_name) if installed_mod and unpack is None: unpack = not installed_mod.packed tmp_file = os.path.join(self.config.factorio_write_path, 'tmp', file_name) os.makedirs(os.path.dirname(tmp_file), exist_ok=True) self.download_mod(release, tmp_file) shutil.move(tmp_file, file_path) mod = ZippedMod(self, file_path) if installed_mod and (installed_mod.basename != mod.basename or not installed_mod.packed): installed_mod.remove() if enable is not None: mod.enabled = enable if unpack: mod.unpack() def validate_mod_file_name(self, file_name): assert '/' not in file_name assert '\\' not in file_name assert file_name.endswith('.zip') def download_mod(self, release, file_path): player_data = self.require_login() url = urljoin(self.api.base_url, release.download_url) basename = release.file_name with ProgressWidget("Downloading: %s..." % basename) as progress: while True: req = self.api.get( url, params={ 'username': player_data['service-username'], 'token': player_data['service-token'] }, stream=True, ) if req.status_code == 403: progress.error() print("Authentication error when downloading mod. " "Please login again.") player_data = self.require_login(reset=True) continue break req.raise_for_status() length = int(req.headers['content-length']) with open(file_path, 'wb') as f: for chunk in req.iter_content(chunk_size=1024): f.write(chunk) progress(f.tell(), length) return ZippedMod(self, file_path)
def _read_info(self): path = os.path.join(self.location, 'info.json') self.info = JSONFile(path)
class DB: def __init__(self, config, api): self.config = config self.api = api self.cache_dir = user_cache_dir('fac', appauthor=False) self.storage = FileStorage(os.path.join(self.cache_dir, 'index')) self.schema = Schema( name=TEXT(sortable=True, phrase=True, field_boost=3, analyzer=intraword), owner=TEXT(sortable=True, field_boost=2.5, analyzer=intraword), title=TEXT(field_boost=2.0, phrase=False), summary=TEXT(phrase=True), downloads=NUMERIC(sortable=True), sort_name=SortColumn(), name_id=ID(stored=True), ) try: self.index = self.storage.open_index() except EmptyIndexError: self.index = None self.db = JSONFile(os.path.join(self.cache_dir, 'mods.json')) def maybe_update(self): if self.needs_update(): self.update() def needs_update(self): if not self.index or not self.db.get('mods'): return True last_update = self.db.mtime period = int(self.config.get('db', 'update_period')) db_age = time.time() - last_update return db_age > period def update(self): with ProgressWidget("Downloading mod database...") as progress: mods = self.api.get_mods(progress) old_mods = self.db.get('mods', {}) self.db.mods = {mod.name: mod.data for mod in mods} if old_mods != self.db['mods']: print("Building search index...") self.index = self.storage.create().create_index(self.schema) with self.index.writer() as w: for mod in mods: w.add_document( name_id=mod.name, name=mod.name, sort_name=mod.name.lower(), title=mod.title.lower(), owner=mod.owner.lower(), summary=mod.summary.lower(), downloads=mod.downloads_count ) self.db.save() print("Updated mods database (%d mods)" % len(mods)) else: print("Index is up to date") self.db.utime() def search(self, query, sortedby=None, limit=None): parser = qparser.MultifieldParser( ['owner', 'name', 'title', 'summary'], schema=self.schema ) parser.add_plugin(qparser.FuzzyTermPlugin()) if not isinstance(query, Query): query = parser.parse(query or 'name:*') with self.index.searcher() as searcher: if sortedby: facets = [] for field in sortedby.split(','): reverse = field.startswith('-') if reverse: field = field[1:] if 'sort_' + field in self.schema: field = 'sort_' + field facets.append(FieldFacet(field, reverse=reverse)) if len(facets) == 1: sortedby = facets[0] else: sortedby = MultiFacet(facets) for result in searcher.search( query, limit=limit, sortedby=sortedby): d = JSONDict(self.db.mods[result['name_id']]) d.score = result.score yield d @property def mods(self): return self.db.mods
class ModManager: """Provides access to the factorio mods directory""" def __init__(self, config, api, db): self.api = api self.config = config self.db = db self.mods_json = None def load(self): self.mods_json = JSONFile( os.path.join( self.config.mods_directory, 'mod-list.json' ) ) if 'mods' not in self.mods_json: self.mods_json.mods = [] def get_mod_json(self, name): """Return the mod json configuration from mods-list.json""" for mod in self.mods_json.mods: if mod.name == name: return mod def get_mod(self, name, *args, **kwargs): for mod in self.find_mods(name, *args, **kwargs): if mod.name == name: return mod def find_mods(self, name=None, version=None, packed=None): mods = [] for mod_type in (ZippedMod, UnpackedMod): if packed is not None and mod_type.packed != bool(packed): continue mods.extend(mod_type.find(self, name, version)) return mods def resolve_mod_name(self, name, remote=False, patterns=True): if patterns and '*' in name: # Keep patterns unmodified return name # Find an exact match mod_names = set(mod.name for mod in self.find_mods()) if remote: mod_names |= set(self.db.mods) if name in mod_names: return name # Find a case-insensitive match for mod_name in mod_names: if mod_name.lower() == name.lower(): return mod_name # Find a unique partial match (case-insensitive) partial_matches = [mod_name for mod_name in mod_names if name.lower() in mod_name.lower()] if len(partial_matches) == 1: return partial_matches[0] if remote: # Find a remote match (case-insensitive) remote_mods = list(self.db.search(name, limit=5)) # If there was only one result, we can assume it's the one if len(remote_mods) == 1: return remote_mods[0].name elif len(remote_mods) > 1: print("'%s' not found, try one of the following:" % name) for match in remote_mods: print(" - " + match.name) print() raise ModNotFoundError(name) # If nothing was found, return original mod name and let things fail return name def resolve_remote_requirement(self, req, ignore_game_ver=False): spec = req.specifier game_ver = None if ignore_game_ver else self.config.game_version_major releases = self.get_releases(req.name, game_ver) yield from ( release for release in releases if release.version in spec ) def resolve_local_requirement(self, req, ignore_game_ver=False): spec = req.specifier game_ver = self.config.game_version_major res = [mod for mod in self.find_mods(req.name) if mod.version in spec and (ignore_game_ver or mod.game_version == game_ver)] res.sort(key=lambda m: m.version, reverse=True) return res def get_releases(self, mod_name, game_version): try: mod = getattr(self.db.mods, mod_name) except AttributeError: raise ModNotFoundError(mod_name) if match_game_version(mod.latest_release, game_version): latest = mod.latest_release yield latest mod = self.api.get_mod(mod_name) res = [release for release in mod.releases if match_game_version(release, game_version) and release.version != latest.version] res.sort(key=lambda r: Version(r.version), reverse=True) yield from res def is_mod_enabled(self, name): mod = self.get_mod_json(name) if mod: # Factorio < 0.15 uses "true"/"false" strings instead of booleans return mod.enabled != 'false' and mod.enabled is not False else: return True # by default, new mods are automatically enabled def set_mod_enabled(self, name, enabled=True): mod = self.get_mod_json(name) if not mod: mod = {'enabled': '', 'name': name} self.mods_json.mods.append(mod) mod = self.get_mod_json(name) if enabled != self.is_mod_enabled(name): if self.config.game_version < Version('0.15'): # Factorio < 0.15 uses "true"/"false" strings # instead of booleans mod.enabled = 'true' if enabled else 'false' else: mod.enabled = enabled self.mods_json.save() return True else: return False def is_mod_held(self, name): return name in self.config.hold def set_mod_held(self, name, held=True): if self.is_mod_held(name) == held: return False if held: self.config.hold.append(name) else: self.config.hold.remove(name) self.config.save() return True def require_login(self, reset=False): import getpass import sys player_data = self.config.player_data username = player_data.get('service-username') token = player_data.get('service-token') if reset or not (username and token): print("You need a Factorio account to download mods.") print("Please provide your username and password to authenticate " "yourself.") print("Your username and token (NOT your password) will be stored " "so that you only have to enter it once") print("This uses the exact same method used by Factorio itself") print() while True: if username: print("Username [%s]:" % username, end=" ", flush=True) else: print("Username:"******" ", flush=True) input_username = sys.stdin.readline().strip() if input_username: username = input_username elif not username: continue password = getpass.getpass("Password (not shown):") if not password: continue try: token = self.api.login(username, password, require_ownership=True) except OwnershipError as ex: print("Ownership error: Your factorio account doesn't " "own the game.") print("Please buy the game or link your Steam account if " "you have bought the game from Steam.") except AuthError as ex: print("Authentication error: %s." % ex) except Exception as ex: print("Error: %s." % ex) else: print("Logged in successfully.") break print() player_data['service-token'] = token player_data['service-username'] = username player_data.save() return player_data def install_mod(self, mod_name, release, enable=None, unpack=None): file_name = release.file_name self.validate_mod_file_name(file_name) file_path = os.path.join(self.config.mods_directory, file_name) installed_mod = self.get_mod(mod_name) if installed_mod and unpack is None: unpack = not installed_mod.packed tmp_file = os.path.join( self.config.factorio_write_path, 'tmp', file_name ) os.makedirs(os.path.dirname(tmp_file), exist_ok=True) self.download_mod(release, tmp_file) shutil.move(tmp_file, file_path) mod = ZippedMod(self, file_path) if installed_mod and (installed_mod.basename != mod.basename or not installed_mod.packed): installed_mod.remove() if enable is not None: mod.enabled = enable if unpack: mod.unpack() def validate_mod_file_name(self, file_name): assert '/' not in file_name assert '\\' not in file_name assert file_name.endswith('.zip') def download_mod(self, release, file_path): player_data = self.require_login() url = urljoin(self.api.base_url, release.download_url) basename = release.file_name with ProgressWidget("Downloading: %s..." % basename) as progress: while True: req = self.api.get( url, params={ 'username': player_data['service-username'], 'token': player_data['service-token'] }, stream=True, ) if req.status_code == 403: progress.error() print("Authentication error when downloading mod. " "Please login again.") player_data = self.require_login(reset=True) continue break req.raise_for_status() length = int(req.headers['content-length']) with open(file_path, 'wb') as f: for chunk in req.iter_content(chunk_size=1024): f.write(chunk) progress(f.tell(), length) return ZippedMod(self, file_path)
class ModManager: 'Provides access to the factorio mods directory' def __init__(self, config, api): self.api = api self.config = config self.mods_json = JSONFile( os.path.join( self.config.mods_path, 'mod-list.json' ) ) def get_mod_json(self, name): """Return the mod json configuration from mods-list.json""" for mod in self.mods_json.mods: if mod.name == name: return mod def get_mod(self, name, *args, **kwargs): for mod in self.find_mods(name, *args, **kwargs): if mod.name == name: return mod def find_mods(self, name=None, version=None, packed=None): mods = [] for mod_type in (ZippedMod, UnpackedMod): if packed is not None and mod_type.packed != bool(packed): continue mods.extend(mod_type.find(self, name, version)) return mods def resolve_mod_name(self, name, remote=False): if '*' in name: # Keep patterns unmodified return name # Find an exact local match local_mods = self.find_mods() for mod in local_mods: if mod.name == name: return name if remote: # Find an exact remote match try: mod = self.api.get(name) return mod.name except ModNotFoundError: pass # Find a local match (case-insensitive) for mod in local_mods: if mod.name.lower() == name.lower(): return mod.name # Find a local partial match (case-insensitive) partial_matches = [mod for mod in local_mods if name.lower() in mod.name.lower()] if len(partial_matches) == 1: return partial_matches[0].name if remote: # Find a remote match (case-insensitive) remote_mods = list(self.api.search(name, page_size=5, limit=5)) for mod in remote_mods: if mod.name.lower() == name.lower(): return mod.name # If there was only one result, we can assume it's the one if len(remote_mods) == 1: return remote_mods[0].name # If nothing was found, return original mod name and let things fail return name def resolve_remote_requirement(self, req, ignore_game_ver=False): spec = req.specifier game_ver = self.config.game_version_major mod = self.api.get(req.name) res = [release for release in mod.releases if release.version in spec and (ignore_game_ver or release.game_version == game_ver)] res.sort(key=lambda r: parse_version(r.version), reverse=True) return res def resolve_local_requirement(self, req, ignore_game_ver=False): spec = req.specifier game_ver = self.config.game_version_major res = [mod for mod in self.find_mods(req.name) if mod.version in spec and (ignore_game_ver or mod.factorio_version == game_ver)] res.sort(key=lambda m: parse_version(m.version), reverse=True) return res def is_mod_enabled(self, name): mod = self.get_mod_json(name) if mod: return mod.enabled != 'false' else: return True # by default, new mods are automatically enabled def set_mod_enabled(self, name, enabled=True): mod = self.get_mod_json(name) if not mod: mod = {'enabled': '', 'name': name} self.mods_json.mods.append(mod) mod = self.get_mod_json(name) if enabled != (mod.enabled == 'true'): mod.enabled = 'true' if enabled else 'false' self.mods_json.save() return True else: return False def is_mod_held(self, name): return name in self.config.hold def set_mod_held(self, name, held=True): if self.is_mod_held(name) == held: return False if held: self.config.hold.append(name) else: self.config.hold.remove(name) self.config.save() return True def require_login(self): import getpass import sys player_data = self.config.player_data username = player_data.get('service-username') token = player_data.get('service-token') if not (username and token): print('You need a Factorio account to download mods.') print('Please provide your username and password to authenticate ' 'yourself.') print('Your username and token (NOT your password) will be stored ' 'so that you only have to enter it once') print('This uses the exact same method used by Factorio itself') print() while True: if username: print('Username [%s]:' % username, end=' ', flush=True) else: print('Username:'******' ', flush=True) input_username = sys.stdin.readline().strip() if input_username: username = input_username elif not username: continue password = getpass.getpass('Password (not shown):') if not password: continue try: token = self.api.login(username, password) except AuthError as ex: print('Authentication error: %s.' % ex) except Exception as ex: print('Error: %s.' % ex) else: print('Logged in successfully.') break print() player_data['service-token'] = token player_data['service-username'] = username player_data.save() return player_data def install_mod(self, release, enable=None, unpack=None): mod_name = release.info_json.name file_name = release.file_name self.validate_mod_file_name(file_name) file_path = os.path.join(self.config.mods_path, file_name) installed_mod = self.get_mod(mod_name) if installed_mod and unpack is None: unpack = not installed_mod.packed tmp_file = os.path.join( self.config.factorio_write_path, 'tmp', file_name ) os.makedirs(os.path.dirname(tmp_file), exist_ok=True) self.download_mod(release, tmp_file) os.rename(tmp_file, file_path) mod = ZippedMod(self, file_path) if installed_mod and (installed_mod.basename != mod.basename or not installed_mod.packed): installed_mod.remove() if enable is not None: mod.enabled = enable if unpack: mod.unpack() def validate_mod_file_name(self, file_name): assert '/' not in file_name assert '\\' not in file_name assert file_name.endswith('.zip') def download_mod(self, release, file_path): player_data = self.require_login() url = urljoin(self.api.base_url, release.download_url) print('Downloading: %s...' % url) req = requests.get( url, params={ 'username': player_data['service-username'], 'token': player_data['service-token'] } ) data = req.content if len(data) != release.file_size: raise Exception( 'Downloaded file has incorrect size (%d), expected %d.' % ( len(data), release.file_size ) ) with open(file_path, 'wb') as f: f.write(data) return ZippedMod(self, file_path)