using LANCommander.Launcher.Data; using LANCommander.Launcher.Data.Models; using LANCommander.Launcher.Models; using LANCommander.SDK.Extensions; using LANCommander.SDK.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LANCommander.Launcher.Services { public class ImportService : BaseService { private readonly MediaService MediaService; private readonly CollectionService CollectionService; private readonly CompanyService CompanyService; private readonly EngineService EngineService; private readonly GameService GameService; private readonly GenreService GenreService; private readonly PlatformService PlatformService; private readonly MultiplayerModeService MultiplayerModeService; private readonly RedistributableService RedistributableService; private readonly TagService TagService; private readonly MessageBusService MessageBusService; private readonly Settings Settings; private readonly DatabaseContext DatabaseContext; public delegate Task OnImportCompleteHandler(); public event OnImportCompleteHandler OnImportComplete; public delegate void OnImportFailedHandler(Exception ex); public event OnImportFailedHandler OnImportFailed; private IEnumerable Collections; private IEnumerable Companies; private IEnumerable Engines; private IEnumerable Genres; private IEnumerable Platforms; private IEnumerable Tags; private IEnumerable MultiplayerModes; public ImportService( SDK.Client client, ILogger logger, MediaService mediaService, CollectionService collectionService, CompanyService companyService, EngineService engineService, GameService gameService, GenreService genreService, PlatformService platformService, MultiplayerModeService multiplayerModeService, RedistributableService redistributableService, TagService tagService, MessageBusService messageBusService, DatabaseContext databaseContext) : base(client, logger) { MediaService = mediaService; CollectionService = collectionService; CompanyService = companyService; EngineService = engineService; GameService = gameService; GenreService = genreService; PlatformService = platformService; MultiplayerModeService = multiplayerModeService; RedistributableService = redistributableService; TagService = tagService; MessageBusService = messageBusService; DatabaseContext = databaseContext; Settings = SettingService.GetSettings(); } public void ImportHasCompleted() { OnImportComplete?.Invoke(); } public async Task ImportAsync() { try { await ImportGamesAsync(); await ImportRedistributables(); OnImportComplete?.Invoke(); } catch (Exception ex) { OnImportFailed?.Invoke(ex); } } private async Task ImportGameAsync(SDK.Models.Game remoteGame, IEnumerable localGames) { using (var op = Logger.BeginOperation("Importing game {GameTitle}", remoteGame.Title)) { try { var localGame = localGames.FirstOrDefault(g => g.Id == remoteGame.Id); if (localGame == null) localGame = new Data.Models.Game(); localGame.Title = remoteGame.Title; localGame.SortTitle = remoteGame.SortTitle; localGame.Description = remoteGame.Description; localGame.Notes = remoteGame.Notes; localGame.ReleasedOn = remoteGame.ReleasedOn; localGame.Type = (Data.Enums.GameType)(int)remoteGame.Type; localGame.Singleplayer = remoteGame.Singleplayer; if (remoteGame.BaseGameId != Guid.Empty && localGame.BaseGameId != remoteGame.BaseGameId) { var baseGame = await GameService.Get(remoteGame.BaseGameId); if (baseGame == null) { await ImportGamesAsync(remoteGame.BaseGameId); baseGame = await GameService.Get(remoteGame.BaseGameId); } localGame.BaseGameId = remoteGame.BaseGameId; } #region Update Game Engine if (remoteGame.Engine == null && localGame.Engine != null) { localGame.Engine = null; localGame.EngineId = null; } else if (remoteGame.Engine != null) { var engine = Engines.FirstOrDefault(e => e.Id == remoteGame.Engine.Id); localGame.Engine = engine; localGame.EngineId = engine.Id; } #endregion #region Update Game Collections if (localGame.Collections == null) localGame.Collections = Collections.Where(c => remoteGame.Collections.Any(rc => rc.Id == c.Id)).ToList(); else { var collectionsToRemove = localGame.Collections.Where(c => !remoteGame.Collections.Any(rc => rc.Id == c.Id)).ToList(); var collectionsToAdd = Collections.Where(c => remoteGame.Collections.Any(rc => rc.Id == c.Id) && !localGame.Collections.Any(lc => lc.Id == c.Id)).ToList(); foreach (var collection in collectionsToRemove) localGame.Collections.Remove(collection); foreach (var collection in collectionsToAdd) localGame.Collections.Add(collection); } #endregion #region Update Game Developers if (localGame.Developers == null) localGame.Developers = Companies.Where(c => remoteGame.Developers.Any(rc => rc.Id == c.Id)).ToList(); else { var developersToRemove = localGame.Developers.Where(c => !remoteGame.Developers.Any(rc => rc.Id == c.Id)).ToList(); var developersToAdd = Companies.Where(c => remoteGame.Developers.Any(rc => rc.Id == c.Id) && !localGame.Developers.Any(lc => lc.Id == c.Id)).ToList(); foreach (var developer in developersToRemove) localGame.Developers.Remove(developer); foreach (var developer in developersToAdd) localGame.Developers.Add(developer); } #endregion #region Update Game Publishers if (localGame.Publishers == null) localGame.Publishers = Companies.Where(c => remoteGame.Publishers.Any(rc => rc.Id == c.Id)).ToList(); else { var publishersToRemove = localGame.Publishers.Where(c => !remoteGame.Publishers.Any(rc => rc.Id == c.Id)).ToList(); var publishersToAdd = Companies.Where(c => remoteGame.Publishers.Any(rc => rc.Id == c.Id) && !localGame.Publishers.Any(lc => lc.Id == c.Id)).ToList(); foreach (var publisher in publishersToRemove) localGame.Publishers.Remove(publisher); foreach (var publisher in publishersToAdd) localGame.Publishers.Add(publisher); } #endregion #region Update Game Genres if (localGame.Genres == null) localGame.Genres = Genres.Where(c => remoteGame.Genres.Any(rc => rc.Id == c.Id)).ToList(); else { var genresToRemove = localGame.Genres.Where(c => !remoteGame.Genres.Any(rc => rc.Id == c.Id)).ToList(); var genresToAdd = Genres.Where(c => remoteGame.Genres.Any(rc => rc.Id == c.Id) && !localGame.Genres.Any(lc => lc.Id == c.Id)).ToList(); foreach (var genre in genresToRemove) localGame.Genres.Remove(genre); foreach (var genre in genresToAdd) localGame.Genres.Add(genre); } #endregion #region Update Game Tags if (localGame.Tags == null) localGame.Tags = Tags.Where(c => remoteGame.Tags.Any(rc => rc.Id == c.Id)).ToList(); else { var tagsToRemove = localGame.Tags.Where(c => !remoteGame.Tags.Any(rc => rc.Id == c.Id)).ToList(); var tagsToAdd = Tags.Where(c => remoteGame.Tags.Any(rc => rc.Id == c.Id) && !localGame.Tags.Any(lc => lc.Id == c.Id)).ToList(); foreach (var tag in tagsToRemove) localGame.Tags.Remove(tag); foreach (var tag in tagsToAdd) localGame.Tags.Add(tag); } #endregion #region Update Game Multiplayer Modes if (localGame.MultiplayerModes == null) localGame.MultiplayerModes = MultiplayerModes.Where(m => remoteGame.MultiplayerModes.Any(rm => rm.Id == m.Id)).ToList(); else { var modesToRemove = localGame.MultiplayerModes.Where(m => !remoteGame.MultiplayerModes.Any(rm => rm.Id == m.Id)).ToList(); var modesToAdd = MultiplayerModes.Where(m => remoteGame.MultiplayerModes.Any(rm => rm.Id == m.Id) && !localGame.MultiplayerModes.Any(lm => lm.Id == m.Id)).ToList(); foreach (var mode in modesToRemove) localGame.MultiplayerModes.Remove(mode); foreach (var mode in modesToAdd) localGame.MultiplayerModes.Add(mode); } #endregion #region Update Play Sessions // This needs to be fixed, the profile ID should _not_ pull out of settings // It'd be better to pull directly from the authenticated client foreach (var session in remoteGame.PlaySessions.Where(rps => rps.UserId == Settings.Profile.Id && !localGame.PlaySessions.Any(lps => lps.Start == rps.Start && lps.End == lps.End))) { localGame.PlaySessions.Add(new PlaySession { Start = session.Start, End = session.End, CreatedOn = session.CreatedOn, UpdatedOn = session.UpdatedOn, GameId = session.GameId, UserId = Settings.Profile.Id }); } #endregion #region Check Installation Status foreach (var installDirectory in Settings.Games.InstallDirectories) { var gameDirectory = await Client.Games.GetInstallDirectory(remoteGame, installDirectory); if (Directory.Exists(gameDirectory)) { var manifestLocation = ManifestHelper.GetPath(gameDirectory, remoteGame.Id); if (File.Exists(manifestLocation)) { var manifest = ManifestHelper.Read(gameDirectory, remoteGame.Id); localGame.Installed = true; localGame.InstalledOn = DateTime.Now; localGame.InstallDirectory = gameDirectory; localGame.InstalledVersion = manifest.Version; } } } #endregion if (localGame.Id == Guid.Empty) { localGame.Id = remoteGame.Id; localGame = await GameService.Add(localGame); } else localGame = await GameService.Update(localGame); } catch (Exception ex) { Logger.LogError(ex, "Could not import game {GameTitle}", remoteGame.Title); } op.Complete(); } } public async Task ImportGamesAsync() { ICollection localGames; IEnumerable remoteGames; Logger?.LogInformation("Importing library games"); using (var op = Logger.BeginOperation("Retrieving games from the database")) { localGames = await GameService.Get(); op.Complete(); } using (var op = Logger.BeginOperation("Retrieving games from the server")) { remoteGames = await Client.Library.GetAsync(); op.Complete(); } await ImportGamesAsync(localGames, remoteGames); } public async Task ImportGamesAsync(params Guid[] ids) { var localGames = new List(); var remoteGames = new List(); foreach (var id in ids) { Guid gameId = id; Game localGame; SDK.Models.Game remoteGame; do { localGame = await GameService.Get(gameId); remoteGame = await Client.Games.GetAsync(gameId); if (localGame != null) localGames.Add(localGame); if (remoteGame != null) remoteGames.Add(remoteGame); if (remoteGame != null && remoteGame.BaseGameId != Guid.Empty) gameId = remoteGame.BaseGameId; } while (remoteGame != null && remoteGame.BaseGameId != Guid.Empty); } await ImportGamesAsync(localGames, remoteGames); } private async Task ImportGamesAsync(IEnumerable localGames, IEnumerable remoteGames) { IEnumerable collections; IEnumerable companies; IEnumerable engines; IEnumerable genres; IEnumerable platforms; IEnumerable tags; IEnumerable multiplayerModes; #region Import Collections using (var op = Logger.BeginOperation("Importing collections")) { Collections = await ImportBulk(remoteGames.SelectMany(g => g.Collections).DistinctBy(c => c.Id), CollectionService, (collection, importCollection) => { collection.Name = importCollection.Name; return collection; }); op.Complete(); } #endregion #region Import Companies using (var op = Logger.BeginOperation("Importing companies")) { var importCompanies = new List(); importCompanies.AddRange(remoteGames.SelectMany(g => g.Developers)); importCompanies.AddRange(remoteGames.SelectMany(g => g.Publishers)); Companies = await ImportBulk(importCompanies.DistinctBy(c => c.Id), CompanyService, (company, importCompany) => { company.Name = importCompany.Name; return company; }); op.Complete(); } #endregion #region Import Engines using (var op = Logger.BeginOperation("Importing engines")) { Engines = await ImportBulk(remoteGames.Where(g => g.Engine != null).Select(g => g.Engine).DistinctBy(e => e.Id), EngineService, (engine, importEngine) => { engine.Name = importEngine.Name; return engine; }); op.Complete(); } #endregion #region Import Genres using (var op = Logger.BeginOperation("Importing genres")) { Genres = await ImportBulk(remoteGames.Where(g => g.Genres != null).SelectMany(g => g.Genres).DistinctBy(g => g.Id), GenreService, (genre, importGenre) => { genre.Name = importGenre.Name; return genre; }); op.Complete(); } #endregion #region Import Platforms using (var op = Logger.BeginOperation("Importing platforms")) { Platforms = await ImportBulk(remoteGames.Where(g => g.Platforms != null).SelectMany(g => g.Platforms).DistinctBy(g => g.Id), PlatformService, (platform, importPlatform) => { platform.Name = importPlatform.Name; return platform; }); op.Complete(); } #endregion #region Import Tags using (var op = Logger.BeginOperation("Importing tags")) { Tags = await ImportBulk(remoteGames.Where(g => g.Tags != null).SelectMany(g => g.Tags).DistinctBy(t => t.Id), TagService, (tag, importTag) => { tag.Name = importTag.Name; return tag; }); op.Complete(); } #endregion #region Import MultiplayerModes using (var op = Logger.BeginOperation("Importing multiplayer modes")) { MultiplayerModes = await ImportBulk(remoteGames.Where(g => g.MultiplayerModes != null).SelectMany(g => g.MultiplayerModes).DistinctBy(t => t.Id), MultiplayerModeService, (multiplayerMode, importMultiplayerMode) => { multiplayerMode.Type = importMultiplayerMode.Type; multiplayerMode.NetworkProtocol = importMultiplayerMode.NetworkProtocol; multiplayerMode.Description = importMultiplayerMode.Description; multiplayerMode.MinPlayers = importMultiplayerMode.MinPlayers; multiplayerMode.MaxPlayers = importMultiplayerMode.MaxPlayers; multiplayerMode.Spectators = importMultiplayerMode.Spectators; return multiplayerMode; }); op.Complete(); } #endregion var importedGames = new List(); using (var gameTransaction = DatabaseContext.Database.BeginTransaction()) { foreach (var remoteGame in remoteGames.OrderBy(g => (int)g.Type)) { if (importedGames.Any(g => g.Id == remoteGame.Id)) continue; if (remoteGame.BaseGameId != Guid.Empty && !importedGames.Any(g => g.Id == remoteGame.BaseGameId)) { var baseGame = remoteGames.FirstOrDefault(g => g.Id == remoteGame.BaseGameId); if (baseGame == null) baseGame = await Client.Games.GetAsync(remoteGame.BaseGameId); await ImportGameAsync(baseGame, localGames); importedGames.Add(baseGame); } await ImportGameAsync(remoteGame, localGames); importedGames.Add(remoteGame); } // Potentially delete any games that no longer exist on the server or have been revoked foreach (var localGame in localGames) { var remoteGame = remoteGames.FirstOrDefault(g => g.Id == localGame.Id); if (remoteGame == null && !localGame.Installed) { using (var op = Logger.BeginOperation("Deleting game {GameTitle}", localGame.Title)) { await GameService.Delete(localGame); op.Complete(); } } } using (var op = Logger.BeginOperation("Committing changes to games")) { await gameTransaction.CommitAsync(); op.Complete(); } } #region Download Media // MediaId, GameId IEnumerable medias; var mediaMap = new Dictionary(); foreach (var importedGame in importedGames) { foreach (var remoteMedia in importedGame.Media) { mediaMap[remoteMedia.Id] = importedGame.Id; } } using (var op = Logger.BeginOperation("Importing media metadata")) { medias = await ImportBulk(importedGames.SelectMany(g => g.Media), MediaService, (media, importMedia) => { media.FileId = importMedia.FileId; media.Type = importMedia.Type; media.SourceUrl = importMedia.SourceUrl; media.MimeType = importMedia.MimeType; media.Crc32 = importMedia.Crc32.ToUpper(); media.Name = importMedia.Name ?? String.Empty; media.GameId = mediaMap.ContainsKey(importMedia.Id) ? mediaMap[importMedia.Id] : Guid.Empty; return media; }, true); op.Complete(); } var mediaStoragePath = MediaService.GetStoragePath(); using (var op = Logger.BeginOperation("Downloading media files")) { foreach (var media in medias) { var localPath = MediaService.GetImagePath(media); if (!File.Exists(localPath) && media.Type != SDK.Enums.MediaType.Manual) { var staleFiles = Directory.EnumerateFiles(mediaStoragePath, $"{media.FileId}-*"); foreach (var staleFile in staleFiles) File.Delete(staleFile); await Client.Media.DownloadAsync(new SDK.Models.Media { Id = media.Id, FileId = media.FileId }, localPath); MessageBusService.MediaChanged(media); } } op.Complete(); } #endregion } public async Task ImportRedistributables() { } public async Task ImportMedia(Guid importMediaId, Guid? gameId = null) { SDK.Models.Game game = null; if (gameId.HasValue) game = await Client.Games.GetAsync(gameId.Value); var media = await Client.Media.Get(importMediaId); return await ImportMedia(media, game); } public async Task ImportMedia(SDK.Models.Media importMedia, SDK.Models.Game game = null) { var media = await MediaService.Get(importMedia.Id); if (media == null) media = new Media(); media.FileId = importMedia.FileId; media.Type = importMedia.Type; media.SourceUrl = importMedia.SourceUrl; media.MimeType = importMedia.MimeType; media.Crc32 = importMedia.Crc32; media.Name = importMedia.Name ?? String.Empty; media.GameId = game?.Id ?? Guid.Empty; if (media.Id == Guid.Empty) { media.Id = importMedia.Id; await MediaService.Add(media); } else await MediaService.Update(media); var mediaStoragePath = MediaService.GetStoragePath(); var localPath = MediaService.GetImagePath(media); if (!File.Exists(localPath) && media.Type != SDK.Enums.MediaType.Manual) { await Client.Media.DownloadAsync(new SDK.Models.Media { Id = media.Id, FileId = media.FileId }, localPath); MessageBusService.MediaChanged(media); } return media; } // Could use something like automapper, but that's slow. public async Task> ImportBulk(IEnumerable importModels, V service, Func additionalMapping, bool clean = true) where T : BaseModel where U : SDK.Models.KeyedModel where V : BaseDatabaseService { // This could be handled better... DI? var models = await service.Get(); foreach (var importModel in importModels) { using (var transaction = DatabaseContext.Database.BeginTransaction()) { try { var model = models.FirstOrDefault(m => m.Id == importModel.Id); if (model == null) model = (T)Activator.CreateInstance(typeof(T)); model = additionalMapping.Invoke(model, importModel); if (model.Id == Guid.Empty) { model.Id = importModel.Id; model = await service.Add(model); } else model = await service.Update(model); transaction.Commit(); } catch (DbUpdateException ex) { transaction.Rollback(); continue; } catch (Exception ex) { } } } if (clean) { foreach (var model in models.Where(m => !importModels.Any(im => im.Id == m.Id))) { try { await service.Delete(model); } catch { } } } // Too slow? return await service.Get(); } } }