LANCommander/LANCommander.Launcher.Services/ImportService.cs
2025-02-06 20:54:03 -06:00

572 lines
21 KiB
C#

using LANCommander.Launcher.Data;
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.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using LANCommander.Launcher.Services.Extensions;
using LANCommander.SDK.Models;
using Microsoft.EntityFrameworkCore.Storage;
using BaseModel = LANCommander.Launcher.Data.Models.BaseModel;
using Collection = LANCommander.Launcher.Data.Models.Collection;
using Company = LANCommander.Launcher.Data.Models.Company;
using Engine = LANCommander.Launcher.Data.Models.Engine;
using Game = LANCommander.Launcher.Data.Models.Game;
using Genre = LANCommander.Launcher.Data.Models.Genre;
using Media = LANCommander.Launcher.Data.Models.Media;
using MultiplayerMode = LANCommander.Launcher.Data.Models.MultiplayerMode;
using Platform = LANCommander.Launcher.Data.Models.Platform;
using PlaySession = LANCommander.Launcher.Data.Models.PlaySession;
using Settings = LANCommander.Launcher.Models.Settings;
using Tag = LANCommander.Launcher.Data.Models.Tag;
namespace LANCommander.Launcher.Services
{
public class ImportService : BaseService
{
private readonly AuthenticationService AuthenticationService;
private readonly MediaService MediaService;
private readonly EngineService EngineService;
private readonly GameService GameService;
private readonly LibraryService LibraryService;
private readonly MessageBusService MessageBusService;
private readonly Settings Settings;
private readonly DatabaseContext DatabaseContext;
private ImportProgress _importProgress = new();
public ImportProgress Progress => _importProgress;
public delegate Task OnImportUpdatedHandler(ImportStatusUpdate update);
public event OnImportUpdatedHandler OnImportUpdated;
public delegate Task OnImportCompleteHandler();
public event OnImportCompleteHandler OnImportComplete;
public delegate void OnImportFailedHandler(Exception ex);
public event OnImportFailedHandler OnImportFailed;
private IEnumerable<Collection> Collections;
private IEnumerable<Company> Companies;
private IEnumerable<Engine> Engines;
private IEnumerable<Genre> Genres;
private IEnumerable<Platform> Platforms;
private IEnumerable<Tag> Tags;
private IEnumerable<MultiplayerMode> MultiplayerModes;
public ImportService(
SDK.Client client,
ILogger<ImportService> logger,
AuthenticationService authenticationService,
LibraryService libraryService,
MediaService mediaService,
EngineService engineService,
GameService gameService,
MessageBusService messageBusService,
DatabaseContext databaseContext) : base(client, logger)
{
AuthenticationService = authenticationService;
LibraryService = libraryService;
MediaService = mediaService;
EngineService = engineService;
GameService = gameService;
MessageBusService = messageBusService;
DatabaseContext = databaseContext;
Settings = SettingService.GetSettings();
}
public void ImportHasCompleted()
{
OnImportComplete?.Invoke();
}
public async Task ImportAsync()
{
try
{
await ImportLibraryAsync();
OnImportComplete?.Invoke();
}
catch (Exception ex)
{
OnImportFailed?.Invoke(ex);
}
}
public async Task<Game> ImportGameAsync(Guid id)
{
var game = await Client.Games.GetAsync(id);
return await ImportGameAsync(game);
}
public async Task<Game> ImportGameAsync(SDK.Models.Game game)
{
using (var op = Logger.BeginOperation("Importing game {GameTitle}", game.Title))
{
try
{
var existing = false;
var localGame = await GameService.GetAsync(game.Id);
if (localGame == null)
localGame = new Game()
{
Id = game.Id,
};
else
existing = true;
localGame.Title = game.Title;
localGame.SortTitle = String.IsNullOrWhiteSpace(game.SortTitle) ? String.Empty : game.SortTitle;
localGame.Description = String.IsNullOrWhiteSpace(game.Description) ? String.Empty : game.Description;
localGame.Notes = String.IsNullOrWhiteSpace(game.Notes) ? String.Empty : game.Notes;
localGame.ReleasedOn = game.ReleasedOn;
localGame.Type = (Data.Enums.GameType)(int)game.Type;
localGame.Singleplayer = game.Singleplayer;
if (!existing)
localGame = await GameService.AddAsync(localGame);
if (game.BaseGameId != Guid.Empty && localGame.BaseGameId != game.BaseGameId)
{
var baseGame = await GameService.GetAsync(game.BaseGameId);
if (baseGame == null)
{
await ImportGameAsync(game.BaseGameId);
localGame.BaseGame = await GameService.GetAsync(game.BaseGameId);
}
else
{
localGame.BaseGame = baseGame;
}
}
#region Update Game Engine
if (game.Engine == null && localGame.Engine != null)
{
localGame.Engine = null;
localGame.EngineId = null;
}
else if (game.Engine != null)
{
var engine = await EngineService.GetAsync(game.Engine.Id);
if (engine != null)
{
localGame.Engine = engine;
localGame.EngineId = engine.Id;
}
}
#endregion
await DatabaseContext.BulkImport<Collection, SDK.Models.Collection>()
.SetTarget(localGame.Collections)
.UseSource(game.Collections)
.Include(c => c.Games)
.Assign((t, s) =>
{
t.Name = s.Name;
t.Games.Add(localGame);
})
.ImportAsync();
await DatabaseContext.BulkImport<Genre, SDK.Models.Genre>()
.SetTarget(localGame.Genres)
.UseSource(game.Genres)
.Include(g => g.Games)
.Assign((t, s) =>
{
t.Name = s.Name;
t.Games.Add(localGame);
})
.ImportAsync();
await DatabaseContext.BulkImport<Company, SDK.Models.Company>()
.SetTarget(localGame.Publishers)
.UseSource(game.Publishers)
.Include(c => c.PublishedGames)
.Assign((t, s) =>
{
t.Name = s.Name;
t.PublishedGames.Add(localGame);
})
.ImportAsync();
await DatabaseContext.BulkImport<Company, SDK.Models.Company>()
.SetTarget(localGame.Developers)
.UseSource(game.Developers)
.Include(c => c.DevelopedGames)
.Assign((t, s) =>
{
t.Name = s.Name;
t.DevelopedGames.Add(localGame);
})
.ImportAsync();
await DatabaseContext.BulkImport<Tag, SDK.Models.Tag>()
.SetTarget(localGame.Tags)
.UseSource(game.Tags)
.Include(t => t.Games)
.Assign((t, s) =>
{
t.Name = s.Name;
t.Games.Add(localGame);
})
.ImportAsync();
await DatabaseContext.BulkImport<MultiplayerMode, SDK.Models.MultiplayerMode>()
.SetTarget(localGame.MultiplayerModes)
.UseSource(game.MultiplayerModes)
.Include(m => m.Game)
.Assign((t, s) =>
{
t.Game = localGame;
t.Description = s.Description;
t.MinPlayers = s.MinPlayers;
t.MaxPlayers = s.MaxPlayers;
t.Spectators = s.Spectators;
t.Type = s.Type;
t.NetworkProtocol = s.NetworkProtocol;
})
.ImportAsync();
await DatabaseContext.BulkImport<Platform, SDK.Models.Platform>()
.SetTarget(localGame.Platforms)
.UseSource(game.Platforms)
.Include(p => p.Games)
.Assign((t, s) =>
{
t.Name = s.Name;
t.Games.Add(localGame);
})
.ImportAsync();
/*await DatabaseContext.BulkImport<PlaySession, SDK.Models.PlaySession>()
.SetTarget(localGame.PlaySessions)
.UseSource(game.PlaySessions)
.Include(p => p.Game)
.Assign((t, s) =>
{
t.Game = localGame;
t.Start = s.Start;
t.End = s.End;
t.UserId = s.UserId;
})
.AsNoRemove()
.ImportAsync();*/
var importedMedia = await DatabaseContext.BulkImport<Media, SDK.Models.Media>()
.SetTarget(localGame.Media)
.UseSource(game.Media)
.Include(m => m.Game)
.Assign((t, s) =>
{
t.Game = localGame;
t.Name = s.Name;
t.Crc32 = s.Crc32;
t.Type = s.Type;
t.FileId = s.FileId;
t.MimeType = s.MimeType;
t.SourceUrl = s.SourceUrl;
})
.AsNoRemove()
.ImportAsync();
foreach (var media in importedMedia)
{
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);
}
}
#region Check Installation Status
foreach (var installDirectory in Settings.Games.InstallDirectories)
{
var gameDirectory = await Client.Games.GetInstallDirectory(game, installDirectory);
if (Directory.Exists(gameDirectory))
{
var manifestLocation = ManifestHelper.GetPath(gameDirectory, game.Id);
if (File.Exists(manifestLocation))
{
var manifest = await ManifestHelper.ReadAsync<SDK.GameManifest>(gameDirectory, game.Id);
localGame.Installed = true;
localGame.InstalledOn = DateTime.Now;
localGame.InstallDirectory = gameDirectory;
localGame.InstalledVersion = manifest.Version;
}
}
}
#endregion
var playSessions = await Client.PlaySessions.GetAsync(localGame.Id);
if (playSessions != null)
{
await DatabaseContext.BulkImport<PlaySession, SDK.Models.PlaySession>()
.SetTarget(localGame.PlaySessions)
.UseSource(playSessions)
.Include(p => p.Game)
.Assign((t, s) =>
{
t.Game = localGame;
t.Start = s.Start;
t.End = s.End;
t.UserId = s.UserId;
})
.AsNoRemove()
.ImportAsync();
}
localGame = await GameService.UpdateAsync(localGame);
return localGame;
}
catch (Exception ex)
{
Logger.LogError(ex, "Could not import game {GameTitle}", game.Title);
return null;
}
finally
{
op.Complete();
}
}
}
public async Task ImportLibraryAsync()
{
var remoteLibrary = await Client.Library.GetAsync();
await ImportLibraryAsync(remoteLibrary);
}
public async Task ImportLibraryAsync(IEnumerable<SDK.Models.EntityReference> games)
{
var library = await LibraryService.GetByUserAsync(AuthenticationService.GetUserId());
Logger?.LogInformation("Starting library import");
_importProgress.IsImporting = true;
_importProgress.Index = 1;
_importProgress.Total = games.Count();
foreach (var game in games)
{
try
{
var remoteGame = await Client.Games.GetAsync(game.Id);
_importProgress.CurrentItem = new ImportItem(game.Id, game.Name);
if (OnImportUpdated != null)
{
await OnImportUpdated.Invoke(new ImportStatusUpdate
{
Index = _importProgress.Index,
Total = _importProgress.Total,
CurrentItem = _importProgress.CurrentItem
});
}
if (game != null && remoteGame != null)
{
var importedGame = await ImportGameAsync(remoteGame);
library.Games.Add(importedGame);
await LibraryService.UpdateAsync(library);
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not import game {GameTitle}", game.Name);
}
finally
{
_importProgress.Index++;
}
}
_importProgress.IsImporting = false;
if (OnImportComplete != null)
await OnImportComplete.Invoke();
Logger?.LogInformation("Importing games completed");
}
public async Task ImportGamesAsync(params Guid[] ids)
{
var games = await Client.Library.GetAsync();
var toImport = new List<EntityReference>();
foreach (var id in ids)
{
var libraryGame = games.FirstOrDefault(g => g.Id == id);
if (libraryGame == null)
{
Logger?.LogInformation("Game with ID {GameId} not currently in library", id);
continue;
}
toImport.Add(libraryGame);
}
await ImportLibraryAsync(toImport);
}
public async Task ImportRedistributables()
{
}
public async Task<Media> ImportMediaAsync(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 ImportMediaAsync(media, game);
}
public async Task<Media> ImportMediaAsync(SDK.Models.Media importMedia, SDK.Models.Game game = null)
{
var media = await MediaService.GetAsync(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.AddAsync(media);
}
else
await MediaService.UpdateAsync(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<IEnumerable<T>> ImportBulk<T, U, V>(IEnumerable<U> importModels, V service, Func<T, U, T> additionalMapping, bool clean = true)
where T : BaseModel
where U : SDK.Models.KeyedModel
where V : BaseDatabaseService<T>
{
// This could be handled better... DI?
var models = await service.GetAsync();
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.AddAsync(model);
}
else
model = await service.UpdateAsync(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.DeleteAsync(model);
}
catch
{
}
}
}
// Too slow?
return await service.GetAsync();
}
}
}