Beispiel #1
0
 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 = []
Beispiel #2
0
 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 = []
Beispiel #3
0
 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'
         )
     )
Beispiel #4
0
    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'))
Beispiel #5
0
Datei: mods.py Projekt: sparr/fac
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)
Beispiel #6
0
Datei: mods.py Projekt: sparr/fac
 def _read_info(self):
     path = os.path.join(self.location, 'info.json')
     self.info = JSONFile(path)
Beispiel #7
0
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
Beispiel #8
0
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)
Beispiel #9
0
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)