def loadInstalled(self) -> None: for path in self.modspath.iterdir(): if path.joinpath('.w3mm').is_file(): mod = Mod.from_json(path.joinpath('.w3mm').read_bytes()) mod.enabled = not path.name.startswith('~') mod.filename = re.sub(r'^(~)', r'', path.name) if mod.enabled: enabled = getSettingsValue( mod.filename, 'Enabled', self.configpath.joinpath('mods.settings')) if enabled == '0': mod.enabled = False self._modList[(mod.filename, mod.target)] = mod else: try: for mod in Mod.fromDirectory(path, recursive=False): mod.installdate = datetime.fromtimestamp( path.stat().st_ctime, tz=timezone.utc) mod.target = 'mods' mod.datatype = 'mod' mod.enabled = not path.name.startswith('~') if mod.enabled: enabled = getSettingsValue( mod.filename, 'Enabled', self.configpath.joinpath('mods.settings')) if enabled == '0': mod.enabled = False self._modList[(mod.filename, mod.target)] = mod asyncio.create_task(self.update(mod)) except InvalidPathError: logger.bind(path=path).debug('Invalid MOD') for path in self.dlcspath.iterdir(): if path.joinpath('.w3mm').is_file(): mod = Mod.from_json(path.joinpath('.w3mm').read_bytes()) mod.enabled = not all( file.name.endswith('.disabled') for file in path.glob('**/*') if file.is_file() and not file.name == '.w3mm') mod.filename = path.name self._modList[(mod.filename, mod.target)] = mod else: try: for mod in Mod.fromDirectory(path, recursive=False): mod.installdate = datetime.fromtimestamp( path.stat().st_ctime, tz=timezone.utc) mod.target = 'dlc' mod.datatype = 'dlc' mod.enabled = not all( file.name.endswith('.disabled') for file in path.glob('**/*') if file.is_file() and not file.name == '.w3mm') self._modList[(mod.filename, mod.target)] = mod asyncio.create_task(self.update(mod)) except InvalidPathError: logger.bind(path=path).debug('Invalid DLC')
async def update(self, mod: Mod) -> None: # serialize and store mod structure target = self.getModPath(mod, True) try: with target.joinpath('.w3mm').open( 'w', encoding='utf-8') as modInfoFile: modSerialized = mod.to_json() modInfoFile.write(modSerialized) except Exception as e: logger.exception(f'Could not update mod: {e}')
async def add(self, mod: Mod) -> None: # TODO: incomplete: always override compilation trigger mod if self.modspath in [mod.source, *mod.source.parents]: raise InvalidSourcePath( mod.source, 'Invalid mod source: Mods cannot be installed from the mods directory' ) async with self.updateLock: if (mod.filename, mod.target) in self._modList: raise ModExistsError(mod.filename, mod.target) target = self.getModPath(mod) if target.exists(): # TODO: incomplete: make sure the mod is tracked by the model raise ModExistsError(mod.filename, mod.target) settings = 0 inputs = 0 try: target.mkdir(parents=True) # copy mod files for _file in mod.files: sourceFile = mod.source.joinpath(_file.source) targetFile = target.joinpath(_file.source) targetFile.parent.mkdir(parents=True, exist_ok=True) copyfile(sourceFile, targetFile) for _content in mod.contents: sourceFile = mod.source.joinpath(_content.source) targetFile = target.joinpath(_content.source) targetFile.parent.mkdir(parents=True, exist_ok=True) copyfile(sourceFile, targetFile) mod.installed = True settings = addSettings( mod.settings, self.configpath.joinpath('user.settings')) inputs = addSettings( mod.inputs, self.configpath.joinpath('input.settings')) setSettingsValue(mod.filename, 'Enabled', '1', self.configpath.joinpath('mods.settings')) await self.update(mod) except Exception as e: removeDirectory(target) if settings: removeSettings(mod.settings, self.configpath.joinpath('user.settings')) if inputs: removeSettings(mod.inputs, self.configpath.joinpath('input.settings')) removeSettingsSection( mod.filename, self.configpath.joinpath('mods.settings')) raise e self._modList[(mod.filename, mod.target)] = mod self.setLastUpdateTime(datetime.now(tz=timezone.utc))
async def updateModDetails(self, mod: Mod) -> bool: logger.bind(name=mod.filename, dots=True).debug('Requesting details for mod') if not mod.md5hash: logger.bind(name=mod.filename).warning( 'Could not get details for mod not installed from archive') return False try: details = await getModInformation(mod.md5hash) except Exception as e: logger.bind(name=mod.filename).warning(f'{e}') return False try: package = str(details[0]['mod']['name']) summary = str(details[0]['mod']['summary']) modid = int(details[0]['mod']['mod_id']) category = int(details[0]['mod']['category_id']) version = str(details[0]['file_details']['version']) fileid = int(details[0]['file_details']['file_id']) uploadname = str(details[0]['file_details']['name']) uploadtime = str(details[0]['file_details']['uploaded_time']) mod.package = package mod.summary = summary mod.modid = modid mod.category = getCategoryName(category) mod.version = version mod.fileid = fileid mod.uploadname = uploadname uploaddate = dateparser.parse(uploadtime) if uploaddate: mod.uploaddate = uploaddate.astimezone(tz=timezone.utc) else: logger.bind(name=mod.filename).debug( f'Could not parse date {uploadtime} in mod information response' ) except KeyError as e: logger.bind(name=mod.filename).exception( f'Could not find key "{str(e)}" in mod information response') return False try: await self.modmodel.update(mod) except Exception as e: logger.bind( name=mod.filename).exception(f'Could not update mod: {e}') return False return True
async def loadInstalledMod(self, path: Path) -> None: if path.joinpath('.w3mm').is_file(): mod = Mod.from_json(path.joinpath('.w3mm').read_bytes()) mod.enabled = not path.name.startswith('~') mod.filename = re.sub(r'^(~)', r'', path.name) if mod.enabled: enabled = getSettingsValue( mod.filename, 'Enabled', self.configpath.joinpath('mods.settings')) if enabled == '0': mod.enabled = False if (mod.filename, mod.target) in self._modList: logger.bind(path=path).error('Ignoring duplicate MOD') if not self._modList[(mod.filename, mod.target)].enabled: self._modList[(mod.filename, mod.target)] = mod else: self._modList[(mod.filename, mod.target)] = mod else: try: for mod in await Mod.fromDirectory(path, recursive=False): mod.installdate = datetime.fromtimestamp( path.stat().st_ctime, tz=timezone.utc) mod.target = 'mods' mod.datatype = 'mod' mod.enabled = not path.name.startswith('~') mod.filename = re.sub(r'^(~)', r'', path.name) if mod.enabled: enabled = getSettingsValue( mod.filename, 'Enabled', self.configpath.joinpath('mods.settings')) if enabled == '0': mod.enabled = False if (mod.filename, mod.target) in self._modList: logger.bind(path=path).error('Ignoring duplicate MOD') if not self._modList[(mod.filename, mod.target)].enabled: self._modList[(mod.filename, mod.target)] = mod else: self._modList[(mod.filename, mod.target)] = mod await self.update(mod) except InvalidPathError: logger.bind(path=path).debug('Invalid MOD')
async def loadInstalledDlc(self, path: Path) -> None: if path.joinpath('.w3mm').is_file(): mod = Mod.from_json(path.joinpath('.w3mm').read_bytes()) mod.enabled = not all( file.name.endswith('.disabled') for file in path.glob('**/*') if file.is_file() and not file.name == '.w3mm') mod.filename = path.name self._modList[(mod.filename, mod.target)] = mod else: try: for mod in await Mod.fromDirectory(path, recursive=False): mod.installdate = datetime.fromtimestamp( path.stat().st_ctime, tz=timezone.utc) mod.target = 'dlc' mod.datatype = 'dlc' mod.enabled = not all( file.name.endswith('.disabled') for file in path.glob('**/*') if file.is_file() and not file.name == '.w3mm') mod.filename = path.name self._modList[(mod.filename, mod.target)] = mod await self.update(mod) except InvalidPathError: logger.bind(path=path).debug('Invalid DLC')
async def installFromFile(self, path: Path, installtime: Optional[datetime] = None) -> Tuple[int, int]: originalpath = path installed = 0 errors = 0 archive = path.is_file() source = None md5hash = '' details = None detailsrequest: Optional[asyncio.Task] = None if not installtime: installtime = datetime.now(tz=timezone.utc) try: if archive: # unpack archive, set source and request details md5hash = getMD5Hash(path) source = path settings = QSettings() if settings.value('nexusGetInfo', 'False') == 'True': logger.bind(path=str(path), dots=True).debug('Requesting details for archive') detailsrequest = asyncio.create_task(getModInformation(md5hash)) logger.bind(path=str(path), dots=True).debug('Unpacking archive') path = await extractMod(source) # validate and read mod valid, exhausted = containsValidMod(path, searchlimit=8) if not valid: if not exhausted and self.showContinueSearchDialog(searchlimit=8): if not containsValidMod(path): raise InvalidPathError(path, 'Invalid mod') elif not exhausted: raise InvalidPathError(path, 'Stopped searching for mod') else: raise InvalidPathError(path, 'Invalid mod') mods = Mod.fromDirectory(path, searchCommonRoot=not archive) installedMods = [] # update mod details and add mods to the model for mod in mods: mod.md5hash = md5hash try: # TODO: incomplete: check if mod is installed, ask if replace await self.modmodel.add(mod) installedMods.append(mod) installed += 1 except ModExistsError: logger.bind(path=source if source else mod.source, name=mod.filename).error(f'Mod exists') errors += 1 continue # wait for details response if requested if detailsrequest: try: details = await detailsrequest except (RequestError, ResponseError, Exception) as e: logger.warning(f'Could not get information for {source.name if source else path.name}: {e}') # update mod with additional information if source or details: for mod in installedMods: if source: # set source if it differs from the scan directory, e.g. an archive mod.source = source if details: # set additional details if requested and available try: package = str(details[0]['mod']['name']) summary = str(details[0]['mod']['summary']) modid = int(details[0]['mod']['mod_id']) category = int(details[0]['mod']['category_id']) version = str(details[0]['file_details']['version']) fileid = int(details[0]['file_details']['file_id']) uploadname = str(details[0]['file_details']['name']) uploadtime = str(details[0]['file_details']['uploaded_time']) mod.package = package mod.summary = summary mod.modid = modid mod.category = getCategoryName(category) mod.version = version mod.fileid = fileid mod.uploadname = uploadname uploaddate = dateparser.parse(uploadtime) if uploaddate: mod.uploaddate = uploaddate.astimezone(tz=timezone.utc) else: logger.bind(name=mod.filename).debug( f'Could not parse date {uploadtime} in mod information response') except KeyError as e: logger.bind(name=mod.filename).exception( f'Could not find key "{str(e)}" in mod information response') try: await self.modmodel.update(mod) except Exception: logger.bind(name=mod.filename).warning('Could not update mod details') except ModelError as e: logger.bind(path=e.path).error(e.message) errors += 1 except InvalidPathError as e: # TODO: enhancement: better install error message logger.bind(path=e.path).error(e.message) errors += 1 except FileNotFoundError as e: logger.bind(path=e.filename).error(e.strerror if e.strerror else str(e)) errors += 1 except OSError as e: logger.bind(path=e.filename).error(e.strerror if e.strerror else str(e)) errors += 1 except Exception as e: logger.exception(str(e)) errors += 1 finally: if detailsrequest and not detailsrequest.done(): detailsrequest.cancel() if archive and not path == originalpath: try: util.removeDirectory(path) except Exception: logger.bind(path=path).warning('Could not remove temporary directory') self.modmodel.setLastUpdateTime(installtime) self.repaint() return installed, errors
async def add(self, mod: Mod) -> None: # TODO: incomplete: always override compilation trigger mod if self.modspath in [mod.source, *mod.source.parents]: raise InvalidSourcePath( mod.source, 'Invalid mod source: Mods cannot be installed from the mods directory' ) async with self.updateLock: if (mod.filename, mod.target) in self._modList: raise ModExistsError(mod.filename, mod.target) target = self.getModPath(mod) if target.exists(): # TODO: incomplete: make sure the mod is tracked by the model raise ModExistsError(mod.filename, mod.target) settings = 0 inputs = 0 try: event_loop = asyncio.get_running_loop() target.mkdir(parents=True) # copy mod files copies = [] logger.bind(name=mod.filename, path=target).debug('Copying binary files') for _file in mod.files: sourceFile = mod.source.joinpath(_file.source) targetFile = target.joinpath(_file.source) targetFile.parent.mkdir(parents=True, exist_ok=True) copies.append((sourceFile, targetFile)) await asyncio.gather(*[ event_loop.run_in_executor( None, partial(copyfile, _copy[0], _copy[1])) for _copy in copies ]) copies = [] logger.bind(name=mod.filename, path=target).debug('Copying content files') for _content in mod.contents: sourceFile = mod.source.joinpath(_content.source) targetFile = target.joinpath(_content.source) targetFile.parent.mkdir(parents=True, exist_ok=True) copies.append((sourceFile, targetFile)) await asyncio.gather(*[ event_loop.run_in_executor( None, partial(copyfile, _copy[0], _copy[1])) for _copy in copies ]) mod.installed = True # update settings logger.bind(name=mod.filename, path=target).debug('Updating settings') settings = addSettings( mod.settings, self.configpath.joinpath('user.settings')) inputs = addSettings( mod.inputs, self.configpath.joinpath('input.settings')) setSettingsValue(mod.filename, 'Enabled', '1', self.configpath.joinpath('mods.settings')) await self.update(mod) except Exception as e: removeDirectory(target) if settings: removeSettings(mod.settings, self.configpath.joinpath('user.settings')) if inputs: removeSettings(mod.inputs, self.configpath.joinpath('input.settings')) removeSettingsSection( mod.filename, self.configpath.joinpath('mods.settings')) raise e self._modList[(mod.filename, mod.target)] = mod self.updateBundledContentsConflicts() self.setLastUpdateTime(datetime.now(tz=timezone.utc))