diff --git a/legendary/cli.py b/legendary/cli.py index 9d8430d..7150832 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -516,7 +516,7 @@ class LegendaryCLI: igame.save_path = save_path self.core.lgd.set_installed_game(igame.app_name, igame) - res, (dt_l, dt_r) = self.core.check_savegame_state(igame.save_path, latest_save.get(igame.app_name)) + res, (dt_l, dt_r) = self.core.check_savegame_state(igame.save_path, igame.save_timestamp, latest_save.get(igame.app_name)) if res == SaveGameStatus.NO_SAVE: logger.info('No cloud or local savegame found.') @@ -526,6 +526,30 @@ class LegendaryCLI: logger.info(f'Save game for "{igame.title}" is up to date, skipping...') continue + if res == SaveGameStatus.CONFLICT and not (args.force_upload or args.force_download): + logger.info(f'Cloud save for "{igame.title}" is in conflict:') + logger.info(f'- Cloud save date: {dt_r.strftime("%Y-%m-%d %H:%M:%S")}') + logger.info(f'- Local save date: {dt_l.strftime("%Y-%m-%d %H:%M:%S")}') + + if args.yes: + logger.warning('Run the command again with appropriate force parameter to effectively pick the save') + continue + else: + result = get_int_choice('Which saves should be kept? Type the number corresponding to preferred action (local - 1/remote - 2/cancel - 3)', + default=3, min_choice=1, max_choice=3) + if result == 1: + self.core.upload_save(igame.app_name, igame.save_path, dt_l, args.disable_filters) + igame.save_timestamp = time.time() + self.core.lgd.set_installed_game(igame.app_name, igame) + elif result == 2: + self.core.download_saves(igame.app_name, save_dir=igame.save_path, clean_dir=True, + manifest_name=latest_save[igame.app_name].manifest_name) + igame.save_timestamp = time.time() + self.core.lgd.set_installed_game(igame.app_name, igame) + else: + logger.info(f'Skipping action for: "{igame.title}"...') + continue + if (res == SaveGameStatus.REMOTE_NEWER and not args.force_upload) or args.force_download: if res == SaveGameStatus.REMOTE_NEWER: # only print this info if not forced logger.info(f'Cloud save for "{igame.title}" is newer:') @@ -547,6 +571,8 @@ class LegendaryCLI: logger.info('Downloading remote savegame...') self.core.download_saves(igame.app_name, save_dir=igame.save_path, clean_dir=True, manifest_name=latest_save[igame.app_name].manifest_name) + igame.save_timestamp = time.time() + self.core.lgd.set_installed_game(igame.app_name, igame) elif res == SaveGameStatus.LOCAL_NEWER or args.force_upload: if res == SaveGameStatus.LOCAL_NEWER: logger.info(f'Local save for "{igame.title}" is newer') @@ -566,6 +592,8 @@ class LegendaryCLI: continue logger.info('Uploading local savegame...') self.core.upload_save(igame.app_name, igame.save_path, dt_l, args.disable_filters) + igame.save_timestamp = time.time() + self.core.lgd.set_installed_game(igame.app_name, igame) def launch_game(self, args, extra): app_name = self._resolve_aliases(args.app_name) diff --git a/legendary/core.py b/legendary/core.py index 04344e3..8c20a0f 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -961,7 +961,7 @@ class LegendaryCore: return absolute_path - def check_savegame_state(self, path: str, save: SaveGameFile) -> (SaveGameStatus, (datetime, datetime)): + def check_savegame_state(self, path: str, sync_timestamp: Optional[float], save: SaveGameFile) -> tuple[SaveGameStatus, tuple[datetime, datetime]]: latest = 0 for _dir, _, _files in os.walk(path): for _file in _files: @@ -971,8 +971,7 @@ class LegendaryCore: if not latest and not save: return SaveGameStatus.NO_SAVE, (None, None) - # timezones are fun! - dt_local = datetime.fromtimestamp(latest).replace(tzinfo=self.local_timezone).astimezone(timezone.utc) + dt_local = datetime.fromtimestamp(latest, tz=timezone.utc) if not save: return SaveGameStatus.LOCAL_NEWER, (dt_local, None) @@ -980,7 +979,15 @@ class LegendaryCore: if not latest: return SaveGameStatus.REMOTE_NEWER, (None, dt_remote) - self.log.debug(f'Local save date: {str(dt_local)}, Remote save date: {str(dt_remote)}') + dt_sync_time = datetime.fromtimestamp(sync_timestamp or 0, tz=timezone.utc) + self.log.debug(f'Local save date: {str(dt_local)}, Remote save date: {str(dt_remote)}, Last sync: {str(dt_sync_time)}') + + # Pickup possible conflict + if sync_timestamp: + remote_updated = (dt_remote - dt_sync_time).total_seconds() > 60 + local_updated = (dt_local - dt_sync_time).total_seconds() > 60 + if remote_updated and local_updated: + return SaveGameStatus.CONFLICT, (dt_local, dt_remote) # Ideally we check the files themselves based on manifest, # this is mostly a guess but should be accurate enough. diff --git a/legendary/models/game.py b/legendary/models/game.py index 5a7eec6..6f94daf 100644 --- a/legendary/models/game.py +++ b/legendary/models/game.py @@ -192,6 +192,7 @@ class InstalledGame: uninstaller: Optional[Dict] = None requires_ot: bool = False save_path: Optional[str] = None + save_timestamp: Optional[float] = None @classmethod def from_json(cls, json): @@ -212,6 +213,7 @@ class InstalledGame: tmp.requires_ot = json.get('requires_ot', False) tmp.is_dlc = json.get('is_dlc', False) tmp.save_path = json.get('save_path', None) + tmp.save_timestamp = json.get('save_timestamp', None) tmp.manifest_path = json.get('manifest_path', '') tmp.needs_verification = json.get('needs_verification', False) is True tmp.platform = json.get('platform', 'Windows') @@ -237,6 +239,7 @@ class SaveGameStatus(Enum): REMOTE_NEWER = 1 SAME_AGE = 2 NO_SAVE = 3 + CONFLICT = 4 class VerifyResult(Enum):