class CleanCommand(Command): """remove old version of mods even if compatible with the game version""" name = 'clean' arguments = [ Arg('-y', '--yes', action='store_true', help="automatic yes to confirmation prompt"), Arg('-U', '--unpacked', action='store_false', dest='packed', default=None, help="only remove unpacked mods"), Arg( '-P', '--packed', action='store_true', dest='packed', default=None, help="only remove packed mods", ) ] def run(self, args): mods = self.manager.find_mods() names = sorted([(mod.name, mod.version) for mod in mods]) to_clean = [] pointer = (None, None) for name in names: if name[0] != pointer[0]: pointer = name else: if name[1] < pointer[1]: to_clean += [name] else: to_clean += [pointer] pointer = name to_clean = [ self.manager.get_mod(mod[0], version=mod[1]) for mod in to_clean ] if not to_clean: print("No old versions of mods to remove.") return else: print("The following mods will be removed:") for mod in to_clean: print(" %s" % mod.location) if not args.yes and prompt("Continue?", "Y/n") != "y": return for mod in to_clean: mod.remove()
class UnholdCommand(Command): """Unhold mods.""" name = 'unhold' arguments = [ Arg('mods', help="mods to unhold", nargs='+'), ] def run(self, args): for mod_pattern in args.mods: mod_pattern = self.manager.resolve_mod_name(mod_pattern) mods = [mod.name for mod in self.manager.find_mods(mod_pattern)] if not mods: # Special case for mods that have been removed # but are still in the hold list if mod_pattern in self.config.hold: mods = [mod_pattern] else: print("No match found for %s." % mod_pattern) continue for mod_name in mods: if self.manager.set_mod_held(mod_name, False): print("%s will now be updated automatically." % mod_name) else: print("%s is not held." % mod_name)
class HoldCommand(Command): """Hold mods (show held mods with no argument).""" name = 'hold' arguments = [ Arg('mods', help="mods patterns to hold", nargs='*'), ] def run(self, args): for mod_pattern in args.mods: mod_pattern = self.manager.resolve_mod_name(mod_pattern) mods = self.manager.find_mods(mod_pattern) if not mods: print("No match found for %s." % mod_pattern) continue for mod in mods: if not mod.held: mod.held = True print("%s will not be updated automatically anymore." % mod.name) else: print("%s is already held" % mod.name) if not args.mods: if self.config.hold: print("Mods currently held:") for name in self.config.hold: print(" %s" % name) else: print("No held mods.")
class EnableDisableCommand(Command): arguments = [ Arg('mods', nargs='+', help="mods patterns to affect"), ] def run(self, args): enabled = self.name == 'enable' for mod_pattern in args.mods: try: mod_name = self.manager.resolve_mod_name(mod_pattern) except ModNotFoundError as e: print("Error: %s" % e) continue mods = self.manager.find_mods(mod_name) if not mods: print("No match found for %s" % mod_pattern) continue for mod in mods: if not self.manager.set_mod_enabled(mod.name, enabled): print("%s was already %sd" % (mod.name, self.name)) else: print("%s is now %sd" % (mod.name, self.name))
class RemoveCommand(Command): """Remove mods.""" name = 'remove' arguments = [ Arg('mods', help="mod patterns to remove ('*' for all)", nargs='+'), Arg('-y', '--yes', action='store_true', help="automatic yes to confirmation prompt"), Arg('-U', '--unpacked', action='store_false', dest='packed', default=None, help="only remove unpacked mods"), Arg( '-P', '--packed', action='store_true', dest='packed', default=None, help="only remove packed mods", ), ] def run(self, args): mods = [] for mod_pattern in args.mods: mod_pattern = self.manager.resolve_mod_name(mod_pattern) matches = self.manager.find_mods(mod_pattern, packed=args.packed) mods.extend(matches) if not matches: print("No match found for %s." % mod_pattern) if mods: print("The following mods will be removed:") for mod in mods: print(" %s" % mod.location) if not args.yes and prompt("Continue?", "Y/n") != "y": return for mod in mods: mod.remove()
class PackUnpackCommand(Command): arguments = [ Arg('mods', nargs='+', help="mods patterns to affect"), Arg('-R', '--replace', action='store_true', help="replace existing file/directory when packing/unpacking"), Arg('-K', '--keep', action='store_true', help="keep existing directory/file after packing/unpacking"), ] def run(self, args): pack = self.name == 'pack' for mod_pattern in args.mods: mod_pattern = self.manager.resolve_mod_name(mod_pattern) mods = self.manager.find_mods(mod_pattern, packed=not pack) if not mods: print("No %sable found for %s." % (self.name, mod_pattern)) continue for mod in mods: dup_mod = self.manager.get_mod(mod.name, mod.version, packed=pack) if dup_mod and not args.replace: print("%s is already %sed. Use -R to replace it." % (mod.name, self.name)) continue if pack: mod.pack(replace=args.replace, keep=args.keep) else: mod.unpack(replace=args.replace, keep=args.keep) print("%s is now %sed" % (mod.name, self.name))
class MakeCompatibleCommand(Command): """ Change the supported factorio version of mods. This modifies the `factorio_version` field in the mods' info.json file to make them compatible with the current game version. Packed mods will be unpacked first. Unpacked mods will be modified in place. """ name = 'make-compatible' arguments = [ Arg('mods', nargs='+', help="mods patterns to affect"), ] def run(self, args): game_ver = self.config.game_version_major for mod_pattern in args.mods: mod_pattern = self.manager.resolve_mod_name(mod_pattern) mods = [ mod.unpack(replace=False) for mod in self.manager.find_mods(mod_pattern) if mod.game_version != game_ver ] if not mods: print("No match for %s." % mod_pattern) continue for mod in mods: print("Game version changed to %s for %s %s." % (game_ver, mod.name, mod.version)) mod.info.factorio_version = game_ver mod.info.save()
class ListCommand(Command): """List installed mods and their status.""" _all_tags = ['disabled', 'unpacked', 'held', 'incompatible'] name = 'list' arguments = [ Arg('-E', '--exclude', metavar='TAG', nargs='+', action='append', default=[], choices=_all_tags, help="exclude mods having any of the specified tags"), Arg('-I', '--include', metavar='TAG', nargs='+', action='append', default=[], choices=_all_tags, help="only include mods having the specified tags"), Arg('-F', '--format', help="show installed mods using the specified format string."), ] epilog = """ Available tags: %s An optional format string can be specified with the -F flag. You can use this if you want to customize the default output format. The syntax of format strings is decribed here: https://docs.python.org/3/library/string.html#format-string-syntax The provided arguments to the format string are: mod : the mod object (see examples) tags : the tags as a space-separated string Using the default string ('s') specifier on a JSON list or object will output valid JSON. Some examples: {tags} {mod.name} Mod name {mod.version} Mod version {mod.game_version} Supported game version {mod.enabled} Is the mod enabled? (True/False) {mod.packed} Is the mod packed? (True/False) {mod.held} Is the mod held? (True/False) {mod.location} Mod file/directory path {mod.info} info.json content as JSON {mod.info.dependencies} """ % (", ".join(_all_tags)) def run(self, args): mods = self.manager.find_mods() if not mods: print("No installed mods.") return if not args.format: if not args.include and not args.exclude: print("Installed mods:") else: print("Matching mods:") found = False for mod in sorted(mods, key=lambda m: (not m.enabled, m.name)): tags = [] if not mod.enabled: tags.append('disabled') if not mod.packed: tags.append('unpacked') if mod.held: tags.append('held') if mod.game_version != self.config.game_version_major: tags.append('incompatible') if any(tag in tags for l in args.exclude for tag in l) or \ any(tag not in tags for l in args.include for tag in l): continue tags = ", ".join(tags) found = True if args.format: print(args.format.format(mod=mod, tags=tags)) else: if tags: tags = " (%s)" % tags print(" %s %s%s" % (mod.name, mod.version, tags)) if not found and not args.format: print("No matches found.")
class UpdateCommand(Command): """Update installed mods.""" name = "update" arguments = [ Arg("-s", "--show", action="store_true", help="only show what would be updated"), Arg("-y", "--yes", action="store_true", help="automatic yes to confirmation prompt"), Arg("-U", "--unpacked", action="store_true", help="allow updating unpacked mods"), Arg("-H", "--held", action="store_true", help="allow updating held mods"), ] def run(self, args): installed = self.manager.find_mods() updates = [] if args.ignore_game_ver: game_ver = None else: game_ver = self.config.game_version_major self.db.update() for local_mod in installed: print("Checking: %s" % local_mod.name) try: release = next( self.manager.get_releases(local_mod.name, game_ver)) except StopIteration: continue found_update = False local_ver = local_mod.version latest_ver = local_ver latest_release = None for release in remote_mod.releases: if not args.ignore_game_ver and \ parse_game_version(release) != game_ver: continue release_ver = Version(release.version) if release_ver > latest_ver: found_update = True latest_ver = release_ver latest_release = release update_mod = True if found_update: print("Found update: %s %s" % (local_mod.name, latest_ver)) if not args.unpacked and not local_mod.packed: print("%s is unpacked. " "Use -U to update it anyway." % (local_mod.name)) update_mod = False if not args.held and local_mod.name in self.config.hold: print("%s is held. " "Use -H to update it anyway." % local_mod.name) update_mod = False if update_mod: updates.append((local_mod, latest_release)) if not updates: print("No updates were found") return print("Found %d update%s:" % ( len(updates), "s" if len(updates) != 1 else "", )) for local_mod, release in updates: print(" %s %s -> %s" % (local_mod.name, local_mod.version, release.version)) if not args.show: if not args.yes and prompt("Continue?", "Y/n") != "y": return for local_mod, release in updates: self.manager.install_mod(local_mod.name, release)
class ShowCommand(Command): """Show details about specific mods.""" name = 'show' arguments = [ Arg('mods', help="mods to show", nargs='+'), Arg('-F', '--format', help="show mods using the specified format string."), Arg('-S', '--sync', help="Force database sync", action='store_true', default=None, dest='sync'), Arg('--no-sync', help="Don't sync database even if it's out of date", action='store_false', default=None, dest='sync'), ] epilog = """ FORMAT STRINGS An optional format string can be specified with the -F flag. You can use this if you want to customize the default output format. The syntax of format strings is decribed here: https://docs.python.org/3/library/string.html#format-string-syntax There is only one argument passed to the format string which is the mod object returned by the API. Using the default string ('s') specifier on a JSON list or object will output valid JSON. Some examples: {mod} JSON as returned by the API {mod.name} Name of the mod {mod.releases[0].version} Version of first release {mod.releases[0].info_json} info.json of first release Note: as a shorthand, you can also use `0` instead of `mod` """ def run(self, args): if args.sync is None: self.db.maybe_update() elif args.sync: self.db.update() first = True for mod in args.mods: if first: first = False else: print("-" * 80) try: mod = self.manager.resolve_mod_name(mod, remote=True, patterns=False) m = self.api.get_mod(mod) except ModNotFoundError as ex: print("Error: %s" % ex) continue if args.format: print(args.format.format(m, mod=m)) continue print("Name: %s" % m.name) print("Author: %s" % m.owner) print("Title: %s" % m.title) print("Summary: %s" % m.summary) # if this ever comes back... if 'description' in m: print("Description:") for line in m.description.splitlines(): print(" %s" % line) if 'tags' in m and m.tags: print("Tags: %s" % ", ".join(tag.name for tag in m.tags)) if 'homepage' in m and m.homepage: print("Homepage: %s" % m.homepage) if 'github_path' in m and m.github_path: print("GitHub page: https://github.com/%s" % m.github_path) if 'license_name' in m: print("License: %s" % m.license_name) game_versions = sorted( set( str(parse_game_version(release)) for release in m.releases)) print("Game versions: %s" % ", ".join(game_versions)) print("Releases:") if not m.releases: print(" No releases") else: for release in m.releases: print(" Version: %-9s Game version: %-9s" % ( release.version, parse_game_version(release), ))
class InstallCommand(Command): """ Install (or update) mods. This will install mods matching the given requirements using this format: name name==version name>=version name<version ... If the version is not specified, the latest version will be selected. Outdated versions will be replaced. """ name = 'install' arguments = [ Arg('requirements', nargs='*', help="requirements to install " '("name", "name>=1.0", "name==1.2", ...)'), Arg('-H', '--held', action='store_true', help="allow updating held mods"), Arg('-R', '--reinstall', action='store_true', help="allow reinstalling mods"), Arg('-D', '--downgrade', action='store_true', help="allow downgrading mods"), Arg('-U', '--unpack', action='store_true', default=None, help="unpack mods zip files"), Arg('-d', '--no-deps', action='store_true', help="do not install any dependencies"), ] def install(self, args, name, release): print("Installing: %s %s..." % (name, release.version)) self.manager.install_mod(name, release, unpack=args.unpack) def run(self, args): to_install = [] for req in args.requirements: name, spec = parse_requirement(req) try: name = self.manager.resolve_mod_name(name, remote=True) req = Requirement(name, spec) releases = start_iter( self.manager.resolve_remote_requirement( req, ignore_game_ver=args.ignore_game_ver)) except StopIteration: releases = [] except ModNotFoundError as ex: print("Error: %s" % ex) continue if not args.held and req.name in self.config.hold: print("%s is held. " "Use -H to install it anyway." % (req.name)) continue local_mod = self.manager.get_mod(req.name) for release in releases: if local_mod: local_ver = local_mod.version release_ver = Version(release.version) if not args.reinstall and release_ver == local_ver: print("%s==%s is already installed. " "Use -R to reinstall it." % (local_mod.name, local_ver)) break elif not args.downgrade and release_ver < local_ver: print( "%s is already installed in a more recent version." " Use -D to downgrade it." % (local_mod.name)) break to_install.append((name, release)) break else: print("No match found for %s" % (req, )) continue for name, release in to_install: self.install(args, name, release) if not args.no_deps: self.install_deps(args) def install_deps(self, args): deps = [] for mod in self.manager.find_mods(): try: deps += mod.info.dependencies self.log.debug("Dependencies needed for %s %s : %s" % (mod.name, mod.version, mod.info.dependencies)) except AttributeError: pass deps_to_install = [] deps_ok = True for dep in deps: depreq = parse_requirement(dep) if depreq.name == 'base': continue # ignore it since it's not like we can install it if depreq.name.startswith('?'): continue # ignore optional dependency if depreq.name.startswith('(?)'): continue # ignore optional dependency if depreq.name.startswith('!'): continue # ignore incompatible dependency print("Resolving needed dependency: %s" % dep) if self.manager.resolve_local_requirement( depreq, ignore_game_ver=args.ignore_game_ver): print("Dependency locally met : %s" % depreq.name) continue try: # FIXME: we only try the first release here releases = self.manager.resolve_remote_requirement( depreq, ignore_game_ver=args.ignore_game_ver) release = next(releases) except ModNotFoundError: print("Dependency not found: %s" % depreq.name) deps_ok = False break except StopIteration: print("Dependency release not found: %s" % depreq.name) print( "Make sure the %s mod is available on mods.factorio.com and compatible with your game version : %s" % (dep, self.config.game_version_major)) deps_ok = False break if not release: print("Dependency can not be met: %s" % depreq) deps_ok = False break if (depreq.name, release) not in deps_to_install: mod = self.manager.get_mod(depreq.name) spec = depreq.specifier if mod.version in spec: print("Dependency already installed : %s %s" % (mod.name, mod.version)) continue print("Installing dependency: %s version %s" % (depreq.name, release.version)) deps_to_install.append((depreq.name, release)) if not deps_ok: return if deps_to_install: print("Installing missing dependencies...") for name, release in deps_to_install: self.install(args, name, release) # we may have added new sub-dependencies self.install_deps(args)
class SearchCommand(Command): """Search the mods database.""" name = 'search' arguments = [ Arg('query', help="search string", default=(), nargs='*'), Arg('-d', help="sort results by most downloaded", action='store_const', dest='sort', const='-downloads'), Arg('-a', help="sort results alphabetically", action='store_const', dest='sort', const='title'), Arg('-u', help="sort results by most recently updated", action='store_const', dest='sort', const='-updated'), Arg('--sort', help="comma-separated list of sort fields. " "Prefix a field with - to reverse it", dest='sort'), Arg('-l', '--limit', type=int, help="stop after returning that many results"), Arg('-F', '--format', help="show results using the specified format string."), Arg('-S', '--sync', help="Force database sync", action='store_true', default=None, dest='sync'), Arg('--no-sync', help="Don't sync database even if it's out of date", action='store_false', default=None, dest='sync'), ] epilog = """ SEARCHING Search query strings use the whoosh library. Full syntax is described here: https://whoosh.readthedocs.io/en/latest/querylang.html You can search by a specific field, eg `summary:blueprint` matches all mods having the word blueprint in their summary. Wildcards can also be used, eg `name:bob*` will match all mods having any word starting with bob in their name. If you don't specify a field, the search will be done on all default fields at the same time: owner, name, title and summary The valid fields are: name: mod name owner: mod owner title: mod title summary: mod summary (not sortable) downloads: download count FORMAT STRINGS An optional format string can be specified with the -F flag. You can use this if you want to customize the default output format. The syntax of format strings is decribed here: https://docs.python.org/3/library/string.html#format-string-syntax There is only one argument passed to the format string which is the result object returned by the API. Using the default string ('s') specifier on a JSON list or object will output valid JSON. Some examples: {result.name} Name of the mod {result} JSON-repesentation of the result object {result.latest_release.version} Latest release version Note: as a shorthand, you can also use `0` instead of `result` """ def run(self, args): sort = args.sort if args.sync is None: self.db.maybe_update() elif args.sync: self.db.update() # null queries just list all mods in alphabetical order by default if not args.query and not sort: sort = 'name' hidden = 0 for result in self.db.search( query=' '.join(args.query), sortedby=sort, limit=args.limit): tags = [] if not match_game_version(result.latest_release, self.config.game_version_major): tags.insert(0, 'incompatible') if not args.ignore_game_ver: hidden += 1 continue if args.format: print(args.format.format(result, result=result)) else: print(result.title) print(" Name: %s" % result.name) if tags: print(" Tags: %s" % (", ".join(tags))) print() print("\n".join( fill( line, width=get_terminal_size()[0] - 4, tabsize=4, subsequent_indent=" ", initial_indent=" ", ) for line in result.summary.splitlines() )) print() if hidden: print("Note: %d mods were hidden because they have no " "compatible game versions. Use -i to show them." % hidden, file=sys.stderr)
class FetchCommand(Command): """ Fetch a mod from the mod portal. This will fetch the mods matching the given requirements using this format: name name==version name>=version name<version ... If the version is not specified, the latest version will be selected. """ name = 'fetch' arguments = [ Arg('requirements', nargs='+', help="requirement to fetch " '("name", "name>=1.0", "name==1.2", ...)'), Arg('-U', '--unpack', action='store_true', default=None, help="unpack mods zip files after downloading"), Arg('-K', '--keep', action='store_true', help="keep mod zip file after unpacking"), Arg('--dest', '-d', default='.', help="destination directory (default: current directory)"), Arg('-R', '--replace', action='store_true', help="replace existing file/directory"), ] def run(self, args): for req in args.requirements: name, spec = parse_requirement(req) try: name = self.manager.resolve_mod_name(name, remote=True) req = Requirement(name, spec) release = next( self.manager.resolve_remote_requirement( req, ignore_game_ver=True)) except ModNotFoundError as ex: print("Error: %s" % ex) continue except StopIteration: print("No match found for %s" % (req, )) continue file_name = release.file_name self.manager.validate_mod_file_name(file_name) file_path = os.path.join(args.dest, file_name) if os.path.exists(file_path) and not args.replace: print("File %s already exists. " "Use -R to replace it." % file_path) continue if not os.path.isdir(args.dest): os.makedirs(args.dest) print("Saving to: %s" % file_path) mod = self.manager.download_mod(release, file_path) if args.unpack: mod.unpack(replace=args.replace, keep=args.keep)
class UpdateCommand(Command): """Update installed mods.""" name = 'update' arguments = [ Arg('-s', '--show', action='store_true', help="only show what would be updated"), Arg('-y', '--yes', action='store_true', help="automatic yes to confirmation prompt"), Arg('-U', '--unpacked', action='store_true', help="allow updating unpacked mods"), Arg('-H', '--held', action='store_true', help="allow updating held mods"), ] def run(self, args): installed = self.manager.find_mods() updates = [] if args.ignore_game_ver: game_ver = None else: game_ver = self.config.game_version_major self.db.update() for local_mod in installed: print("Checking: %s" % local_mod.name) try: release = next(self.manager.get_releases(local_mod.name, game_ver)) except StopIteration: continue release_ver = Version(release.version) local_ver = local_mod.version if release_ver > local_ver: print("Found update: %s %s" % ( local_mod.name, release.version) ) if not args.unpacked and not local_mod.packed: print( "%s is unpacked. " "Use -U to update it anyway." % ( local_mod.name ) ) continue if not args.held and local_mod.name in self.config.hold: print("%s is held. " "Use -H to update it anyway." % local_mod.name) break updates.append((local_mod, release)) break if not updates: print("No updates were found") return print("Found %d update%s:" % ( len(updates), "s" if len(updates) != 1 else "", )) for local_mod, release in updates: print(" %s %s -> %s" % ( local_mod.name, local_mod.version, release.version )) if not args.show: if not args.yes and prompt("Continue?", "Y/n") != "y": return for local_mod, release in updates: self.manager.install_mod(local_mod.name, release)