From 6dfc0906aca8253a157a65d48fc3a5025eaee60b Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 29 Nov 2025 18:24:27 -0600 Subject: [PATCH] Refactor launcher library importing, manifests Switched the library importing in the launcher to work based on a queue. As a new game is added to the queue, it should find any related data and also add it to the queue. This should result in an easier to maintain importer while reducing the load on the launcher's DAL. This may result in more RAM usage while importing. Local manifests have also been refactored to match manifests in import/export LCX files, removing the old manifest format. This is a large breaking change that will probably break existing game installations. A migration will probably have to be created. --- LANCommander.Launcher.Data/Enums/GameType.cs | 16 - .../20251130001620_AddImportedOn.Designer.cs | 852 ++++++++++++++++++ .../20251130001620_AddImportedOn.cs | 180 ++++ .../DatabaseContextModelSnapshot.cs | 43 +- .../Models/BaseModel.cs | 3 + LANCommander.Launcher.Data/Models/Game.cs | 6 +- LANCommander.Launcher.Data/Models/Media.cs | 3 +- .../Models/MultiplayerMode.cs | 3 +- .../ActionSelectorDialogOptions.cs | 2 +- .../CommandLineService.cs | 10 +- .../Exceptions/ImportSkippedException.cs | 17 + .../Extensions/ServiceCollectionExtensions.cs | 23 +- .../FilterService.cs | 3 +- LANCommander.Launcher.Services/GameService.cs | 2 +- .../Import/Factories/ImportContextFactory.cs | 13 + .../Import/IImportItemInfo.cs | 9 + .../Import/IImporter.cs | 11 + .../Import/ImportContext.cs | 150 +++ .../Import/ImportItemInfo.cs | 10 + .../Import}/ImportStatusUpdate.cs | 4 +- .../Import/Importers/BaseImporter.cs | 37 + .../Import/Importers/CollectionImporter.cs | 74 ++ .../Import/Importers/DeveloperImporter.cs | 74 ++ .../Import/Importers/EngineImporter.cs | 74 ++ .../Import/Importers/GameImporter.cs | 87 ++ .../Import/Importers/GenreImporter.cs | 74 ++ .../Importers/MultiplayerModeImporter.cs | 84 ++ .../Import/Importers/PlatformImporter.cs | 74 ++ .../Import/Importers/PublisherImporter.cs | 74 ++ .../Import/Importers/TagImporter.cs | 74 ++ .../ImportService.cs | 649 +------------ .../Models/LibraryFilter.cs | 3 +- .../PowerShell/ElevatedScriptInterceptor.cs | 2 +- .../_BaseDatabaseService.cs | 11 +- .../UI/Components/ActionSelectorDialog.razor | 4 +- .../UI/Components/ImportHandler.razor | 19 +- .../Library/LibraryItemContextMenu.razor | 17 +- .../UI/Components/MediaImage.razor | 4 +- .../UI/Components/PlayButton.razor | 20 +- .../Components/RemoveFromLibraryDialog.razor | 5 +- .../Depot/Components/DepotGameDetails.razor | 5 +- .../Depot/Components/DepotGameItem.razor | 2 +- .../Models/GameInstallationArchive.cs | 5 +- .../Models/GameInstallationFileList.cs | 2 +- LANCommander.SDK/Models/GameManifest.cs | 56 -- LANCommander.SDK/Models/Manifest/Game.cs | 3 +- .../Models/Manifest/Redistributable.cs | 2 +- LANCommander.SDK/Models/Manifest/Server.cs | 2 +- LANCommander.SDK/Models/SavePathEntry.cs | 7 + .../PowerShell/Cmdlets/Get-GameManifest.cs | 4 +- .../PowerShell/Cmdlets/Write-GameManifest.cs | 2 +- LANCommander.SDK/ProcessExecutionContext.cs | 4 +- LANCommander.SDK/Services/GameClient.cs | 104 ++- LANCommander.SDK/Services/SaveClient.cs | 12 +- LANCommander.SDK/Services/ScriptClient.cs | 24 +- LANCommander.SDK/Utilities/SavePackager.cs | 10 +- .../Factories/ImportContextFactory.cs | 2 +- .../ArchiveService.cs | 9 +- LANCommander.Server.Services/GameService.cs | 132 +-- .../Endpoints/GameEndpoints.cs | 6 +- 60 files changed, 2229 insertions(+), 984 deletions(-) delete mode 100644 LANCommander.Launcher.Data/Enums/GameType.cs create mode 100644 LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.Designer.cs create mode 100644 LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.cs create mode 100644 LANCommander.Launcher.Services/Exceptions/ImportSkippedException.cs create mode 100644 LANCommander.Launcher.Services/Import/Factories/ImportContextFactory.cs create mode 100644 LANCommander.Launcher.Services/Import/IImportItemInfo.cs create mode 100644 LANCommander.Launcher.Services/Import/IImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/ImportContext.cs create mode 100644 LANCommander.Launcher.Services/Import/ImportItemInfo.cs rename {LANCommander.Launcher.Models => LANCommander.Launcher.Services/Import}/ImportStatusUpdate.cs (50%) create mode 100644 LANCommander.Launcher.Services/Import/Importers/BaseImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/CollectionImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/DeveloperImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/EngineImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/GameImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/GenreImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/MultiplayerModeImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/PlatformImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/PublisherImporter.cs create mode 100644 LANCommander.Launcher.Services/Import/Importers/TagImporter.cs delete mode 100644 LANCommander.SDK/Models/GameManifest.cs create mode 100644 LANCommander.SDK/Models/SavePathEntry.cs diff --git a/LANCommander.Launcher.Data/Enums/GameType.cs b/LANCommander.Launcher.Data/Enums/GameType.cs deleted file mode 100644 index cad57435..00000000 --- a/LANCommander.Launcher.Data/Enums/GameType.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace LANCommander.Launcher.Data.Enums -{ - public enum GameType - { - [Display(Name = "Main Game")] - MainGame, - Expansion, - [Display(Name = "Standalone Expansion")] - StandaloneExpansion, - Mod, - [Display(Name = "Standalone Mod")] - StandaloneMod - } -} diff --git a/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.Designer.cs b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.Designer.cs new file mode 100644 index 00000000..ab1e9299 --- /dev/null +++ b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.Designer.cs @@ -0,0 +1,852 @@ +// +using System; +using LANCommander.Launcher.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LANCommander.Launcher.Data.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251130001620_AddImportedOn")] + partial class AddImportedOn + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("CategoryGame", b => + { + b.Property("CategoriesId") + .HasColumnType("TEXT"); + + b.Property("GamesId") + .HasColumnType("TEXT"); + + b.HasKey("CategoriesId", "GamesId"); + + b.HasIndex("GamesId"); + + b.ToTable("CategoryGame"); + }); + + modelBuilder.Entity("CollectionGame", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "GameId"); + + b.HasIndex("GameId"); + + b.ToTable("CollectionGame"); + }); + + modelBuilder.Entity("GameDeveloper", b => + { + b.Property("DeveloperId") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("TEXT"); + + b.HasKey("DeveloperId", "GameId"); + + b.HasIndex("GameId"); + + b.ToTable("GameDeveloper"); + }); + + modelBuilder.Entity("GameGenre", b => + { + b.Property("GamesId") + .HasColumnType("TEXT"); + + b.Property("GenresId") + .HasColumnType("TEXT"); + + b.HasKey("GamesId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("GameGenre"); + }); + + modelBuilder.Entity("GamePlatform", b => + { + b.Property("GamesId") + .HasColumnType("TEXT"); + + b.Property("PlatformsId") + .HasColumnType("TEXT"); + + b.HasKey("GamesId", "PlatformsId"); + + b.HasIndex("PlatformsId"); + + b.ToTable("GamePlatform"); + }); + + modelBuilder.Entity("GamePublisher", b => + { + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("PublisherId") + .HasColumnType("TEXT"); + + b.HasKey("GameId", "PublisherId"); + + b.HasIndex("PublisherId"); + + b.ToTable("GamePublisher"); + }); + + modelBuilder.Entity("GameRedistributable", b => + { + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("RedistributableId") + .HasColumnType("TEXT"); + + b.HasKey("GameId", "RedistributableId"); + + b.HasIndex("RedistributableId"); + + b.ToTable("GameRedistributable"); + }); + + modelBuilder.Entity("GameTag", b => + { + b.Property("GamesId") + .HasColumnType("TEXT"); + + b.Property("TagsId") + .HasColumnType("TEXT"); + + b.HasKey("GamesId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("GameTag"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Collections"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Companies"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Engine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Engines"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BaseGameId") + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("EngineId") + .HasColumnType("TEXT"); + + b.Property("IGDBId") + .HasColumnType("INTEGER"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("InstallDirectory") + .HasColumnType("TEXT"); + + b.Property("Installed") + .HasColumnType("INTEGER"); + + b.Property("InstalledOn") + .HasColumnType("TEXT"); + + b.Property("InstalledVersion") + .HasColumnType("TEXT"); + + b.Property("LatestVersion") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("ReleasedOn") + .HasColumnType("TEXT"); + + b.Property("Singleplayer") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BaseGameId"); + + b.HasIndex("EngineId"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Genres"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Libraries"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Crc32") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("FileId") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SourceUrl") + .HasMaxLength(2048) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.MultiplayerMode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("MaxPlayers") + .HasColumnType("INTEGER"); + + b.Property("MinPlayers") + .HasColumnType("INTEGER"); + + b.Property("NetworkProtocol") + .HasColumnType("INTEGER"); + + b.Property("Spectators") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("MultiplayerModes"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Platform", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Platforms"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.PlaySession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("End") + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GameId"); + + b.ToTable("PlaySessions"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Redistributable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Redistributables"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .HasColumnType("TEXT"); + + b.Property("ImportedOn") + .HasColumnType("TEXT"); + + b.Property("UpdatedOn") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("LibraryGame", b => + { + b.Property("GameId") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("TEXT"); + + b.HasKey("GameId", "LibraryId"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryGame"); + }); + + modelBuilder.Entity("CategoryGame", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GamesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionGame", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Collection", null) + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDeveloper", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Company", null) + .WithMany() + .HasForeignKey("DeveloperId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameGenre", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GamesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GamePlatform", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GamesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Platform", null) + .WithMany() + .HasForeignKey("PlatformsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GamePublisher", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Company", null) + .WithMany() + .HasForeignKey("PublisherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameRedistributable", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Redistributable", null) + .WithMany() + .HasForeignKey("RedistributableId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameTag", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GamesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Category", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Game", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", "BaseGame") + .WithMany("DependentGames") + .HasForeignKey("BaseGameId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("LANCommander.Launcher.Data.Models.Engine", "Engine") + .WithMany("Games") + .HasForeignKey("EngineId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("BaseGame"); + + b.Navigation("Engine"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Library", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.User", "User") + .WithOne("Library") + .HasForeignKey("LANCommander.Launcher.Data.Models.Library", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Media", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game") + .WithMany("Media") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("LANCommander.Launcher.Data.Models.User", "User") + .WithOne("Avatar") + .HasForeignKey("LANCommander.Launcher.Data.Models.Media", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.MultiplayerMode", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game") + .WithMany("MultiplayerModes") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.PlaySession", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", "Game") + .WithMany("PlaySessions") + .HasForeignKey("GameId"); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("LibraryGame", b => + { + b.HasOne("LANCommander.Launcher.Data.Models.Game", null) + .WithMany() + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LANCommander.Launcher.Data.Models.Library", null) + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Category", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Engine", b => + { + b.Navigation("Games"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.Game", b => + { + b.Navigation("DependentGames"); + + b.Navigation("Media"); + + b.Navigation("MultiplayerModes"); + + b.Navigation("PlaySessions"); + }); + + modelBuilder.Entity("LANCommander.Launcher.Data.Models.User", b => + { + b.Navigation("Avatar"); + + b.Navigation("Library"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.cs b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.cs new file mode 100644 index 00000000..bad830ab --- /dev/null +++ b/LANCommander.Launcher.Data/Migrations/20251130001620_AddImportedOn.cs @@ -0,0 +1,180 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LANCommander.Launcher.Data.Migrations +{ + /// + public partial class AddImportedOn : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Users", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Tags", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Redistributables", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "PlaySessions", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Platforms", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "MultiplayerModes", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Media", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Libraries", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Genres", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AlterColumn( + name: "ImportedOn", + table: "Games", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + oldClrType: typeof(DateTime), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Engines", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Companies", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Collections", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "ImportedOn", + table: "Categories", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Users"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Tags"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Redistributables"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "PlaySessions"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Platforms"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "MultiplayerModes"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Media"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Libraries"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Genres"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Engines"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Companies"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Collections"); + + migrationBuilder.DropColumn( + name: "ImportedOn", + table: "Categories"); + + migrationBuilder.AlterColumn( + name: "ImportedOn", + table: "Games", + type: "TEXT", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "TEXT"); + } + } +} diff --git a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs index f80e68d3..efe3f132 100644 --- a/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs +++ b/LANCommander.Launcher.Data/Migrations/DatabaseContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace LANCommander.Launcher.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); modelBuilder.Entity("CategoryGame", b => { @@ -146,6 +146,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -172,6 +175,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -193,6 +199,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -214,6 +223,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -247,7 +259,7 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("IGDBId") .HasColumnType("INTEGER"); - b.Property("ImportedOn") + b.Property("ImportedOn") .HasColumnType("TEXT"); b.Property("InstallDirectory") @@ -305,6 +317,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -326,6 +341,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("UpdatedOn") .HasColumnType("TEXT"); @@ -360,6 +378,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("GameId") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("MimeType") .HasMaxLength(255) .HasColumnType("TEXT"); @@ -406,6 +427,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("GameId") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("MaxPlayers") .HasColumnType("INTEGER"); @@ -440,6 +464,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -467,6 +494,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("GameId") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Start") .HasColumnType("TEXT"); @@ -495,6 +525,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("Description") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -519,6 +552,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); @@ -543,6 +579,9 @@ namespace LANCommander.Launcher.Data.Migrations b.Property("CreatedOn") .HasColumnType("TEXT"); + b.Property("ImportedOn") + .HasColumnType("TEXT"); + b.Property("UpdatedOn") .HasColumnType("TEXT"); diff --git a/LANCommander.Launcher.Data/Models/BaseModel.cs b/LANCommander.Launcher.Data/Models/BaseModel.cs index c1190774..d6d7fe48 100644 --- a/LANCommander.Launcher.Data/Models/BaseModel.cs +++ b/LANCommander.Launcher.Data/Models/BaseModel.cs @@ -8,6 +8,9 @@ namespace LANCommander.Launcher.Data.Models { [Key] public Guid Id { get; set; } + + [Display(Name = "Imported On")] + public DateTime ImportedOn { get; set; } [Display(Name = "Created On")] public DateTime CreatedOn { get; set; } diff --git a/LANCommander.Launcher.Data/Models/Game.cs b/LANCommander.Launcher.Data/Models/Game.cs index 3dc5ffec..b5539c78 100644 --- a/LANCommander.Launcher.Data/Models/Game.cs +++ b/LANCommander.Launcher.Data/Models/Game.cs @@ -1,6 +1,6 @@ -using LANCommander.Launcher.Data.Enums; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using LANCommander.SDK.Enums; namespace LANCommander.Launcher.Data.Models { @@ -23,8 +23,6 @@ namespace LANCommander.Launcher.Data.Models [Display(Name = "Released On")] public DateTime? ReleasedOn { get; set; } - - public DateTime? ImportedOn { get; set; } public GameType Type { get; set; } public Guid? BaseGameId { get; set; } diff --git a/LANCommander.Launcher.Data/Models/Media.cs b/LANCommander.Launcher.Data/Models/Media.cs index a42bbb82..7a9742e2 100644 --- a/LANCommander.Launcher.Data/Models/Media.cs +++ b/LANCommander.Launcher.Data/Models/Media.cs @@ -1,5 +1,4 @@ -using LANCommander.Launcher.Data.Enums; -using LANCommander.SDK.Enums; +using LANCommander.SDK.Enums; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; diff --git a/LANCommander.Launcher.Data/Models/MultiplayerMode.cs b/LANCommander.Launcher.Data/Models/MultiplayerMode.cs index a794ec52..7fc034a6 100644 --- a/LANCommander.Launcher.Data/Models/MultiplayerMode.cs +++ b/LANCommander.Launcher.Data/Models/MultiplayerMode.cs @@ -1,5 +1,4 @@ -using LANCommander.Launcher.Data.Enums; -using LANCommander.SDK.Enums; +using LANCommander.SDK.Enums; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; diff --git a/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs b/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs index a1e5bbe1..deaf099e 100644 --- a/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs +++ b/LANCommander.Launcher.Models/ActionSelectorDialogOptions.cs @@ -9,7 +9,7 @@ namespace LANCommander.Launcher.Models { public class ActionSelectorDialogOptions { - public IEnumerable Actions { get; set; } + public IEnumerable Actions { get; set; } public string Title { get; set; } public Game Game { get; set; } } diff --git a/LANCommander.Launcher.Services/CommandLineService.cs b/LANCommander.Launcher.Services/CommandLineService.cs index 6cefa831..d3a75406 100644 --- a/LANCommander.Launcher.Services/CommandLineService.cs +++ b/LANCommander.Launcher.Services/CommandLineService.cs @@ -123,14 +123,14 @@ namespace LANCommander.Launcher.Services try { - var game = await gameService.GetAsync(options.GameId); - var manifest = await ManifestHelper.ReadAsync(game.InstallDirectory, game.Id); + /*var game = await gameService.GetAsync(options.GameId); + var manifest = await ManifestHelper.ReadAsync(game.InstallDirectory, game.Id); var action = manifest.Actions.FirstOrDefault(a => a.Id == options.ActionId); if (action == null) action = manifest.Actions.OrderBy(a => a.SortOrder).FirstOrDefault(a => a.IsPrimaryAction); - await gameService.Run(game, action); + await gameService.Run(game, action);*/ } catch (Exception ex) { @@ -142,7 +142,7 @@ namespace LANCommander.Launcher.Services { Logger.LogInformation("Syncing games from server..."); - importService.OnImportComplete += async () => + /*importService.OnImportComplete += async () => { Logger.LogInformation("Sync complete!"); }; @@ -150,7 +150,7 @@ namespace LANCommander.Launcher.Services importService.OnImportFailed += async (Exception ex) => { Logger.LogError(ex, "Sync failed!"); - }; + };*/ await importService.ImportAsync(); } diff --git a/LANCommander.Launcher.Services/Exceptions/ImportSkippedException.cs b/LANCommander.Launcher.Services/Exceptions/ImportSkippedException.cs new file mode 100644 index 00000000..125b3ea5 --- /dev/null +++ b/LANCommander.Launcher.Services/Exceptions/ImportSkippedException.cs @@ -0,0 +1,17 @@ +namespace LANCommander.Launcher.Services.Exceptions; + +public class ImportSkippedException : Exception where TRecord : class +{ + public TRecord Record { get; set; } + + public ImportSkippedException(TRecord record, string message) : base(message) + { + Record = record; + } + + public ImportSkippedException(TRecord record, string message, Exception innerException) : base(message, + innerException) + { + Record = record; + } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs index dd028458..b62d3676 100644 --- a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs +++ b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ using LANCommander.Launcher.Data; +using LANCommander.Launcher.Services.Import; +using LANCommander.Launcher.Services.Import.Factories; +using LANCommander.Launcher.Services.Import.Importers; using LANCommander.Launcher.Services.PowerShell; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -17,7 +20,10 @@ namespace LANCommander.Launcher.Services.Extensions { public static IServiceCollection AddLANCommanderLauncher(this IServiceCollection services, Action configure) { - services.AddDbContext(); + services.AddDbContext(options => + { + options.EnableSensitiveDataLogging(); + }); #region Register Client var options = new LANCommanderOptions(); @@ -55,6 +61,21 @@ namespace LANCommander.Launcher.Services.Extensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + + #region Import + + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + #endregion return services; } diff --git a/LANCommander.Launcher.Services/FilterService.cs b/LANCommander.Launcher.Services/FilterService.cs index c3710bcb..5ac99b9c 100644 --- a/LANCommander.Launcher.Services/FilterService.cs +++ b/LANCommander.Launcher.Services/FilterService.cs @@ -1,5 +1,6 @@ using LANCommander.Launcher.Data.Models; using LANCommander.Launcher.Models; +using LANCommander.SDK.Enums; using LANCommander.SDK.Extensions; using Microsoft.Extensions.Logging; @@ -116,7 +117,7 @@ namespace LANCommander.Launcher.Services if (Filter.Installed) items = items.Where(i => (i.DataItem as Game).Installed); - items = items.Where(i => (i.DataItem as Game).Type.ValueIsIn(Data.Enums.GameType.MainGame, Data.Enums.GameType.StandaloneExpansion, Data.Enums.GameType.StandaloneMod)); + items = items.Where(i => (i.DataItem as Game).Type.ValueIsIn(GameType.MainGame, GameType.StandaloneExpansion, GameType.StandaloneMod)); op.Complete(); } diff --git a/LANCommander.Launcher.Services/GameService.cs b/LANCommander.Launcher.Services/GameService.cs index 6a82328a..80e13484 100644 --- a/LANCommander.Launcher.Services/GameService.cs +++ b/LANCommander.Launcher.Services/GameService.cs @@ -65,7 +65,7 @@ namespace LANCommander.Launcher.Services } } - public async Task Run(Game game, SDK.Models.Action action) + public async Task Run(Game game, SDK.Models.Manifest.Action action) { Guid userId; diff --git a/LANCommander.Launcher.Services/Import/Factories/ImportContextFactory.cs b/LANCommander.Launcher.Services/Import/Factories/ImportContextFactory.cs new file mode 100644 index 00000000..c41b94e8 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Factories/ImportContextFactory.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace LANCommander.Launcher.Services.Import.Factories; + +public class ImportContextFactory(IServiceProvider serviceProvider) +{ + public ImportContext Create() + { + var scope = serviceProvider.CreateScope(); + + return scope.ServiceProvider.GetRequiredService(); + } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/IImportItemInfo.cs b/LANCommander.Launcher.Services/Import/IImportItemInfo.cs new file mode 100644 index 00000000..347f6385 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/IImportItemInfo.cs @@ -0,0 +1,9 @@ +namespace LANCommander.Launcher.Services.Import; + +public interface IImportItemInfo +{ + string Key { get; set; } + string Type { get; set; } + string Name { get; set; } + bool Processed { get; set; } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/IImporter.cs b/LANCommander.Launcher.Services/Import/IImporter.cs new file mode 100644 index 00000000..91be2030 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/IImporter.cs @@ -0,0 +1,11 @@ +namespace LANCommander.Launcher.Services.Import; + +public interface IImporter where TRecord : class +{ + void UseContext(ImportContext importContext); + Task> GetImportInfoAsync(TRecord record); + Task CanImportAsync(TRecord record); + Task AddAsync(TRecord record); + Task UpdateAsync(TRecord record); + Task ExistsAsync(TRecord record); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/ImportContext.cs b/LANCommander.Launcher.Services/Import/ImportContext.cs new file mode 100644 index 00000000..076a0be0 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/ImportContext.cs @@ -0,0 +1,150 @@ +using LANCommander.Launcher.Services.Import.Importers; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.DependencyInjection; + +namespace LANCommander.Launcher.Services.Import; + +public class ImportContext +{ + public IImportItemInfo CurrentItem { get; set; } + public int Processed => _queue.Count(qi => qi.Processed); + public int Total => _queue.Count; + + public EventHandler OnImportStarted { get; set; } + public EventHandler OnImportProgress { get; set; } + public EventHandler OnImportCompleted { get; set; } + + private List _queue { get; set; } = new(); + + #region Importers + + private CollectionImporter _collections; + private DeveloperImporter _developers; + private EngineImporter _engines; + private GameImporter _games; + private GenreImporter _genres; + private MultiplayerModeImporter _multiplayerModes; + private PlatformImporter _platforms; + private PublisherImporter _publishers; + private TagImporter _tags; + #endregion + + public ImportContext(IServiceProvider serviceProvider) + { + _collections = serviceProvider.GetRequiredService(); + _developers = serviceProvider.GetRequiredService(); + _engines = serviceProvider.GetRequiredService(); + _games = serviceProvider.GetRequiredService(); + _genres = serviceProvider.GetRequiredService(); + _multiplayerModes = serviceProvider.GetRequiredService(); + _platforms = serviceProvider.GetRequiredService(); + _publishers = serviceProvider.GetRequiredService(); + _tags = serviceProvider.GetRequiredService(); + } + + public async Task AddAsync(Game game) + { + foreach (var collection in game.Collections) + if (!InQueue(collection, _collections) && await _collections.CanImportAsync(collection)) + _queue.Add(await _collections.GetImportInfoAsync(collection)); + + foreach (var developer in game.Developers) + if (!InQueue(developer, _developers) && await _developers.CanImportAsync(developer)) + _queue.Add(await _developers.GetImportInfoAsync(developer)); + + if (!InQueue(game.Engine, _engines) && await _engines.CanImportAsync(game.Engine)) + _queue.Add(await _engines.GetImportInfoAsync(game.Engine)); + + foreach (var genre in game.Genres) + if (!InQueue(genre, _genres) && await _genres.CanImportAsync(genre)) + _queue.Add(await _genres.GetImportInfoAsync(genre)); + + foreach (var multiplayerMode in game.MultiplayerModes) + if (!InQueue(multiplayerMode, _multiplayerModes) && await _multiplayerModes.CanImportAsync(multiplayerMode)) + _queue.Add(await _multiplayerModes.GetImportInfoAsync(multiplayerMode)); + + foreach (var platform in game.Platforms) + if (!InQueue(platform, _platforms) && await _platforms.CanImportAsync(platform)) + _queue.Add(await _platforms.GetImportInfoAsync(platform)); + + foreach (var publisher in game.Publishers) + if (!InQueue(publisher, _publishers) && await _publishers.CanImportAsync(publisher)) + _queue.Add(await _publishers.GetImportInfoAsync(publisher)); + + foreach (var tag in game.Tags) + if (!InQueue(tag, _tags) && await _tags.CanImportAsync(tag)) + _queue.Add(await _tags.GetImportInfoAsync(tag)); + + if (!InQueue(game, _games) && await _games.CanImportAsync(game)) + _queue.Add(await _games.GetImportInfoAsync(game)); + } + + private bool InQueue(TRecord record, BaseImporter importer) + where TRecord : class => + _queue.Any(qi => qi.Key == importer.GetKey(record)); + + public async Task ImportQueueAsync() + { + OnImportStarted?.Invoke(this, EventArgs.Empty); + + // Import metadata before games + _queue = _queue + .OrderBy(qi => qi.Type == nameof(Game)) + .ThenBy(qi => qi.Type) + .ThenBy(qi => qi.Name) + .ToList(); + + foreach (var queueItem in _queue) + { + CurrentItem = queueItem; + + OnImportProgress?.Invoke(this, new ImportStatusUpdate + { + CurrentItem = CurrentItem, + Index = Processed, + Total = Total, + }); + + switch (queueItem.Type) + { + case nameof(Collection): + await _collections.ImportAsync(queueItem); + break; + + case "Developer": + await _developers.ImportAsync(queueItem); + break; + + case nameof(Engine): + await _engines.ImportAsync(queueItem); + break; + + case nameof(Game): + await _games.ImportAsync(queueItem); + break; + + case nameof(Genre): + await _genres.ImportAsync(queueItem); + break; + + case nameof(MultiplayerMode): + await _multiplayerModes.ImportAsync(queueItem); + break; + + case nameof(Platform): + await _platforms.ImportAsync(queueItem); + break; + + case "Publisher": + await _publishers.ImportAsync(queueItem); + break; + + case nameof(Tag): + await _tags.ImportAsync(queueItem); + break; + } + } + + OnImportCompleted?.Invoke(this, EventArgs.Empty); + } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/ImportItemInfo.cs b/LANCommander.Launcher.Services/Import/ImportItemInfo.cs new file mode 100644 index 00000000..3c62f5a0 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/ImportItemInfo.cs @@ -0,0 +1,10 @@ +namespace LANCommander.Launcher.Services.Import; + +public class ImportItemInfo : IImportItemInfo where T : class +{ + public string Key { get; set; } + public string Type { get; set; } + public string Name { get; set; } + public bool Processed { get; set; } + public T Record { get; set; } +} \ No newline at end of file diff --git a/LANCommander.Launcher.Models/ImportStatusUpdate.cs b/LANCommander.Launcher.Services/Import/ImportStatusUpdate.cs similarity index 50% rename from LANCommander.Launcher.Models/ImportStatusUpdate.cs rename to LANCommander.Launcher.Services/Import/ImportStatusUpdate.cs index 030b2a9a..55ca092a 100644 --- a/LANCommander.Launcher.Models/ImportStatusUpdate.cs +++ b/LANCommander.Launcher.Services/Import/ImportStatusUpdate.cs @@ -1,8 +1,8 @@ -namespace LANCommander.Launcher.Models; +namespace LANCommander.Launcher.Services.Import; public class ImportStatusUpdate { public int Index { get; set; } public int Total { get; set; } - public ImportItem CurrentItem { get; set; } + public IImportItemInfo CurrentItem { get; set; } } \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/BaseImporter.cs b/LANCommander.Launcher.Services/Import/Importers/BaseImporter.cs new file mode 100644 index 00000000..20f38bf7 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/BaseImporter.cs @@ -0,0 +1,37 @@ +using LANCommander.Launcher.Services.Exceptions; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public abstract class BaseImporter : IImporter where TRecord : class +{ + protected ImportContext ImportContext { get; private set; } + + public void UseContext(ImportContext importContext) + { + ImportContext = importContext; + } + + public async Task ImportAsync(IImportItemInfo importItem) + { + if (importItem is ImportItemInfo importItemInfo) + { + if (await ExistsAsync(importItemInfo.Record)) + { + importItem.Processed = true; + return await UpdateAsync(importItemInfo.Record); + } + + importItem.Processed = true; + return await AddAsync(importItemInfo.Record); + } + + throw new ImportSkippedException(null, "Import item record is not supported by this importer."); + } + + public abstract string GetKey(TRecord record); + public abstract Task> GetImportInfoAsync(TRecord record); + public abstract Task CanImportAsync(TRecord record); + public abstract Task AddAsync(TRecord record); + public abstract Task UpdateAsync(TRecord record); + public abstract Task ExistsAsync(TRecord record); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/CollectionImporter.cs b/LANCommander.Launcher.Services/Import/Importers/CollectionImporter.cs new file mode 100644 index 00000000..ad52407d --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/CollectionImporter.cs @@ -0,0 +1,74 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class CollectionImporter( + CollectionService collectionService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Collection record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Name, + Type = nameof(Collection), + Record = record, + }; + } + + public override string GetKey(Collection record) => $"{nameof(Collection)}/{record.Name}"; + + public override async Task CanImportAsync(Collection record) + { + var existing = await collectionService.FirstOrDefaultAsync(c => c.Name == record.Name); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Collection record) + { + var collection = new Data.Models.Collection + { + Name = record.Name, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await collectionService.AddAsync(collection); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add collection", ex); + } + } + + public override async Task UpdateAsync(Collection record) + { + var existing = await collectionService.FirstOrDefaultAsync(c => c.Name == record.Name); + + try + { + existing.Name = record.Name; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update collection", ex); + } + } + + public override async Task ExistsAsync(Collection record) => await collectionService.ExistsAsync(c => c.Name == record.Name); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/DeveloperImporter.cs b/LANCommander.Launcher.Services/Import/Importers/DeveloperImporter.cs new file mode 100644 index 00000000..d1504196 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/DeveloperImporter.cs @@ -0,0 +1,74 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class DeveloperImporter( + CompanyService companyService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Company record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Name, + Type = "Developer", + Record = record, + }; + } + + public override string GetKey(Company record) => $"{nameof(Company)}/{record.Name}"; + + public override async Task CanImportAsync(Company record) + { + var existing = await companyService.FirstOrDefaultAsync(c => c.Name == record.Name); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Company record) + { + var company = new Data.Models.Company + { + Name = record.Name, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await companyService.AddAsync(company); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add company", ex); + } + } + + public override async Task UpdateAsync(Company record) + { + var existing = await companyService.FirstOrDefaultAsync(c => c.Name == record.Name); + + try + { + existing.Name = record.Name; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update company", ex); + } + } + + public override async Task ExistsAsync(Company record) => await companyService.ExistsAsync(c => c.Name == record.Name); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/EngineImporter.cs b/LANCommander.Launcher.Services/Import/Importers/EngineImporter.cs new file mode 100644 index 00000000..6d365f80 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/EngineImporter.cs @@ -0,0 +1,74 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class EngineImporter( + EngineService engineService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Engine record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Name, + Type = nameof(Engine), + Record = record, + }; + } + + public override string GetKey(Engine record) => $"{nameof(Engine)}/{record.Name}"; + + public override async Task CanImportAsync(Engine record) + { + var existing = await engineService.FirstOrDefaultAsync(c => c.Name == record.Name); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Engine record) + { + var engine = new Data.Models.Engine + { + Name = record.Name, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await engineService.AddAsync(engine); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add engine", ex); + } + } + + public override async Task UpdateAsync(Engine record) + { + var existing = await engineService.FirstOrDefaultAsync(c => c.Name == record.Name); + + try + { + existing.Name = record.Name; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update engine", ex); + } + } + + public override async Task ExistsAsync(Engine record) => await engineService.ExistsAsync(c => c.Name == record.Name); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/GameImporter.cs b/LANCommander.Launcher.Services/Import/Importers/GameImporter.cs new file mode 100644 index 00000000..23adbf0b --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/GameImporter.cs @@ -0,0 +1,87 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class GameImporter( + GameService gameService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Game record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Title, + Type = nameof(Game), + Record = record, + }; + } + + public override string GetKey(Game record) => $"{nameof(Game)}/{record.Id}"; + + public override async Task CanImportAsync(Game record) + { + var existing = await gameService.GetAsync(record.Id); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Game record) + { + var game = new Data.Models.Game + { + Title = record.Title, + SortTitle = record.SortTitle, + Description = record.Description, + Notes = record.Notes, + ReleasedOn = record.ReleasedOn, + Singleplayer = record.Singleplayer, + Type = record.Type, + IGDBId = record.IGDBId, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await gameService.AddAsync(game); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add game", ex); + } + } + + public override async Task UpdateAsync(Game record) + { + var existing = await gameService.GetAsync(record.Id); + + try + { + existing.Title = record.Title; + existing.SortTitle = record.SortTitle; + existing.Description = record.Description; + existing.Notes = record.Notes; + existing.ReleasedOn = record.ReleasedOn; + existing.Singleplayer = record.Singleplayer; + existing.Type = record.Type; + existing.IGDBId = record.IGDBId; + existing.CreatedOn = record.CreatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update game", ex); + } + } + + public override async Task ExistsAsync(Game record) => await gameService.ExistsAsync(record.Id); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/GenreImporter.cs b/LANCommander.Launcher.Services/Import/Importers/GenreImporter.cs new file mode 100644 index 00000000..d53b3544 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/GenreImporter.cs @@ -0,0 +1,74 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class GenreImporter( + GenreService genreService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Genre record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Name, + Type = nameof(Genre), + Record = record, + }; + } + + public override string GetKey(Genre record) => $"{nameof(Genre)}/{record.Name}"; + + public override async Task CanImportAsync(Genre record) + { + var existing = await genreService.FirstOrDefaultAsync(c => c.Name == record.Name); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Genre record) + { + var genre = new Data.Models.Genre + { + Name = record.Name, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await genreService.AddAsync(genre); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add genre", ex); + } + } + + public override async Task UpdateAsync(Genre record) + { + var existing = await genreService.FirstOrDefaultAsync(c => c.Name == record.Name); + + try + { + existing.Name = record.Name; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update genre", ex); + } + } + + public override async Task ExistsAsync(Genre record) => await genreService.ExistsAsync(c => c.Name == record.Name); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/MultiplayerModeImporter.cs b/LANCommander.Launcher.Services/Import/Importers/MultiplayerModeImporter.cs new file mode 100644 index 00000000..e3d50a8e --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/MultiplayerModeImporter.cs @@ -0,0 +1,84 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class MultiplayerModeImporter( + MultiplayerModeService multiplayerModeService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(MultiplayerMode record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = $"{record.NetworkProtocol} - {record.Type}", + Type = nameof(MultiplayerMode), + Record = record, + }; + } + + public override string GetKey(MultiplayerMode record) => $"{nameof(Collection)}/{record.NetworkProtocol}:{record.Type}"; + + public override async Task CanImportAsync(MultiplayerMode record) + { + var existing = await multiplayerModeService.FirstOrDefaultAsync(mm => mm.Type == record.Type && mm.NetworkProtocol == record.NetworkProtocol); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(MultiplayerMode record) + { + var multiplayerMode = new Data.Models.MultiplayerMode + { + Type = record.Type, + Description = record.Description, + MaxPlayers = record.MaxPlayers, + MinPlayers = record.MinPlayers, + NetworkProtocol = record.NetworkProtocol, + Spectators = record.Spectators, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await multiplayerModeService.AddAsync(multiplayerMode); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add multiplayer mode", ex); + } + } + + public override async Task UpdateAsync(MultiplayerMode record) + { + var existing = await multiplayerModeService.FirstOrDefaultAsync(mm => mm.Type == record.Type && mm.NetworkProtocol == record.NetworkProtocol); + + try + { + existing.Type = record.Type; + existing.Description = record.Description; + existing.MaxPlayers = record.MaxPlayers; + existing.MinPlayers = record.MinPlayers; + existing.NetworkProtocol = record.NetworkProtocol; + existing.Spectators = record.Spectators; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update multiplayer mode", ex); + } + } + + public override async Task ExistsAsync(MultiplayerMode record) => await multiplayerModeService.ExistsAsync(mm => mm.Type == record.Type && mm.NetworkProtocol == record.NetworkProtocol); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/PlatformImporter.cs b/LANCommander.Launcher.Services/Import/Importers/PlatformImporter.cs new file mode 100644 index 00000000..169cc0af --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/PlatformImporter.cs @@ -0,0 +1,74 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class PlatformImporter( + PlatformService platformService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Platform record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Name, + Type = nameof(Platform), + Record = record, + }; + } + + public override string GetKey(Platform record) => $"{nameof(Platform)}/{record.Name}"; + + public override async Task CanImportAsync(Platform record) + { + var existing = await platformService.FirstOrDefaultAsync(c => c.Name == record.Name); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Platform record) + { + var platform = new Data.Models.Platform + { + Name = record.Name, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await platformService.AddAsync(platform); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add platform", ex); + } + } + + public override async Task UpdateAsync(Platform record) + { + var existing = await platformService.FirstOrDefaultAsync(c => c.Name == record.Name); + + try + { + existing.Name = record.Name; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update platform", ex); + } + } + + public override async Task ExistsAsync(Platform record) => await platformService.ExistsAsync(c => c.Name == record.Name); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/PublisherImporter.cs b/LANCommander.Launcher.Services/Import/Importers/PublisherImporter.cs new file mode 100644 index 00000000..a23f0332 --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/PublisherImporter.cs @@ -0,0 +1,74 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class PublisherImporter( + CompanyService companyService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Company record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Name, + Type = "Publisher", + Record = record, + }; + } + + public override string GetKey(Company record) => $"{nameof(Company)}/{record.Name}"; + + public override async Task CanImportAsync(Company record) + { + var existing = await companyService.FirstOrDefaultAsync(c => c.Name == record.Name); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Company record) + { + var company = new Data.Models.Company + { + Name = record.Name, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow, + }; + + try + { + return await companyService.AddAsync(company); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add company", ex); + } + } + + public override async Task UpdateAsync(Company record) + { + var existing = await companyService.FirstOrDefaultAsync(c => c.Name == record.Name); + + try + { + existing.Name = record.Name; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update company", ex); + } + } + + public override async Task ExistsAsync(Company record) => await companyService.ExistsAsync(c => c.Name == record.Name); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/Import/Importers/TagImporter.cs b/LANCommander.Launcher.Services/Import/Importers/TagImporter.cs new file mode 100644 index 00000000..0606e95c --- /dev/null +++ b/LANCommander.Launcher.Services/Import/Importers/TagImporter.cs @@ -0,0 +1,74 @@ +using LANCommander.Launcher.Services.Exceptions; +using LANCommander.SDK.Models.Manifest; +using Microsoft.Extensions.Logging; + +namespace LANCommander.Launcher.Services.Import.Importers; + +public class TagImporter( + TagService tagService, + ILogger logger) : BaseImporter +{ + public override async Task> GetImportInfoAsync(Tag record) + { + return new ImportItemInfo + { + Key = GetKey(record), + Name = record.Name, + Type = nameof(Tag), + Record = record, + }; + } + + public override string GetKey(Tag record) => $"{nameof(Tag)}/{record.Name}"; + + public override async Task CanImportAsync(Tag record) + { + var existing = await tagService.FirstOrDefaultAsync(c => c.Name == record.Name); + + if (existing == null) + return true; + + return record.UpdatedOn > existing.ImportedOn; + } + + public override async Task AddAsync(Tag record) + { + var tag = new Data.Models.Tag + { + Name = record.Name, + CreatedOn = record.CreatedOn, + UpdatedOn = record.UpdatedOn, + ImportedOn = DateTime.UtcNow + }; + + try + { + return await tagService.AddAsync(tag); + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to add tag", ex); + } + } + + public override async Task UpdateAsync(Tag record) + { + var existing = await tagService.FirstOrDefaultAsync(c => c.Name == record.Name); + + try + { + existing.Name = record.Name; + existing.CreatedOn = record.CreatedOn; + existing.UpdatedOn = record.UpdatedOn; + existing.ImportedOn = DateTime.UtcNow; + + return existing; + } + catch (Exception ex) + { + throw new ImportSkippedException(record, "An unknown error occurred while trying to update tag", ex); + } + } + + public override async Task ExistsAsync(Tag record) => await tagService.ExistsAsync(c => c.Name == record.Name); +} \ No newline at end of file diff --git a/LANCommander.Launcher.Services/ImportService.cs b/LANCommander.Launcher.Services/ImportService.cs index 10f91f3a..3ea26ec2 100644 --- a/LANCommander.Launcher.Services/ImportService.cs +++ b/LANCommander.Launcher.Services/ImportService.cs @@ -1,48 +1,31 @@ -using LANCommander.Launcher.Data; -using LANCommander.Launcher.Models; -using LANCommander.SDK.Extensions; -using LANCommander.SDK.Helpers; -using Microsoft.EntityFrameworkCore; +using LANCommander.Launcher.Models; using Microsoft.Extensions.Logging; -using LANCommander.Launcher.Services.Extensions; -using LANCommander.SDK; -using LANCommander.SDK.Models; -using BaseModel = LANCommander.Launcher.Data.Models.BaseModel; +using LANCommander.Launcher.Services.Import; +using LANCommander.Launcher.Services.Import.Factories; +using LANCommander.SDK.Services; 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.Settings.Settings; using Tag = LANCommander.Launcher.Data.Models.Tag; namespace LANCommander.Launcher.Services { public class ImportService( ILogger logger, - DatabaseContext databaseContext, - LibraryService libraryService, - MediaService mediaService, - EngineService engineService, - GameService gameService, - MessageBusService messageBusService, - SDK.Client client) : BaseService(logger) + ImportContextFactory importContextFactory, + GameClient gameClient, + LibraryClient libraryClient) : BaseService(logger) { 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 Task OnImportFailedHandler(Exception ex); - public event OnImportFailedHandler OnImportFailed; + public EventHandler OnImportStarted; + public EventHandler OnImportStatusUpdate; + public EventHandler OnImportComplete; + public EventHandler OnImportError; private IEnumerable Collections; private IEnumerable Companies; @@ -52,633 +35,45 @@ namespace LANCommander.Launcher.Services private IEnumerable Tags; private IEnumerable MultiplayerModes; - public void ImportHasCompleted() - { - OnImportComplete?.Invoke(); - } - public async Task ImportAsync() { try { await ImportLibraryAsync(); - OnImportComplete?.Invoke(); + OnImportComplete?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { - OnImportFailed?.Invoke(ex); - } - } - - public async Task ImportGameAsync(Guid id) - { - var game = await client.Games.GetAsync(id); - - return await ImportGameAsync(game); - } - - public async Task 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; - - if (localGame.ImportedOn > game.UpdatedOn) - { - Logger.LogInformation($"Skipping {game.Title}, no new information to grab"); - return localGame; - } - - 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.ImportedOn = DateTime.UtcNow; - 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; - } - } - - var childGamesIds = game.DependentGames.ToArray(); - foreach (var childGameId in childGamesIds) - { - await ImportGameAsync(childGameId); - } - - #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() - .SetTarget(localGame.Collections) - .UseSource(game.Collections) - .AsBatch() - .Assign((t, s) => - { - t.Name = s.Name; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - // Check if the relationship already exists - var linked = databaseContext.Entry(t) - .Collection(x => x.Games) - .Query() - .Any(g => g.Id == localGame.Id); - - if (!linked) - t.Games.Add(localGame); - }) - .ImportAsync(); - - await databaseContext.BulkImport() - .SetTarget(localGame.Genres) - .UseSource(game.Genres) - .AsBatch() - .Assign((t, s) => - { - t.Name = s.Name; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - // Check if the relationship already exists - var linked = databaseContext.Entry(t) - .Collection(x => x.Games) - .Query() - .Any(g => g.Id == localGame.Id); - - if (!linked) - t.Games.Add(localGame); - }) - .ImportAsync(); - - await databaseContext.BulkImport() - .SetTarget(localGame.Publishers) - .UseSource(game.Publishers) - .AsBatch() - .Assign((t, s) => - { - t.Name = s.Name; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - // Check if the relationship already exists - var linked = databaseContext.Entry(t) - .Collection(x => x.PublishedGames) - .Query() - .Any(g => g.Id == localGame.Id); - - if (!linked) - t.PublishedGames.Add(localGame); - }) - .ImportAsync(); - - await databaseContext.BulkImport() - .SetTarget(localGame.Developers) - .UseSource(game.Developers) - .AsBatch() - .Assign((t, s) => - { - t.Name = s.Name; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - // Check if the relationship already exists - var linked = databaseContext.Entry(t) - .Collection(x => x.DevelopedGames) - .Query() - .Any(g => g.Id == localGame.Id); - - if (!linked) - t.DevelopedGames.Add(localGame); - }) - .ImportAsync(); - - await databaseContext.BulkImport() - .SetTarget(localGame.Tags) - .UseSource(game.Tags) - .AsBatch() - .Assign((t, s) => - { - t.Name = s.Name; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - // Check if the relationship already exists - var linked = databaseContext.Entry(t) - .Collection(x => x.Games) - .Query() - .Any(g => g.Id == localGame.Id); - - if (!linked) - t.Games.Add(localGame); - }) - .ImportAsync(); - - await databaseContext.BulkImport() - .SetTarget(localGame.MultiplayerModes) - .UseSource(game.MultiplayerModes) - .AsBatch() - .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; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - var linked = t.GameId == localGame.Id; - - if (!linked) - t.Game = localGame; - }) - .ImportAsync(); - - await databaseContext.BulkImport() - .SetTarget(localGame.Platforms) - .UseSource(game.Platforms) - .AsBatch() - .Assign((t, s) => - { - t.Name = s.Name; - t.Games.Add(localGame); - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - // Check if the relationship already exists - var linked = databaseContext.Entry(t) - .Collection(x => x.Games) - .Query() - .Any(g => g.Id == localGame.Id); - - if (!linked) - t.Games.Add(localGame); - }) - .ImportAsync(); - - /*await databaseContext.BulkImport() - .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() - .SetTarget(localGame.Media) - .UseSource(game.Media) - .AsBatch() - .Assign((t, s) => - { - t.Name = s.Name; - t.Crc32 = s.Crc32; - t.Type = s.Type; - t.FileId = s.FileId; - t.MimeType = s.MimeType; - t.SourceUrl = s.SourceUrl; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - var linked = t.GameId == localGame.Id; - - if (!linked) - t.Game = localGame; - }) - .AsNoRemove() - .ImportAsync(); - - foreach (var media in importedMedia) - { - var localPath = client.Media.GetLocalPath(media.FileId, media.Crc32); - - 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 client.Settings.CurrentValue.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(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() - .SetTarget(localGame.PlaySessions) - .UseSource(playSessions) - .Include(p => p.Game) - .Assign((t, s) => - { - t.Start = s.Start; - t.End = s.End; - t.UserId = s.UserId; - }) - .AssignRelationships((t, s) => - { - // Ensure the game is tracked before checking relationships - if (databaseContext.Entry(localGame).State == EntityState.Detached) - databaseContext.Attach(localGame); - - var linked = t.GameId == localGame.Id; - - if (!linked) - t.Game = localGame; - }) - .AsNoRemove() - .ImportAsync(); - } - - // Save all pending changes from batch operations - await databaseContext.SaveChangesAsync(); - - 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(); - } + OnImportError?.Invoke(this, ex); } } public async Task ImportLibraryAsync() { - var remoteLibrary = await client.Library.GetAsync(); + var remoteLibrary = await libraryClient.GetAsync(); - try - { - await ImportLibraryAsync(remoteLibrary); - } - catch (Exception ex) - { - Logger.LogError(ex, "Could not import library!"); - OnImportFailed?.Invoke(ex); - } - } - - public async Task ImportLibraryAsync(IEnumerable games) - { Logger?.LogInformation("Starting library import"); - _importProgress.IsImporting = true; - _importProgress.Index = 1; - _importProgress.Total = games.Count(); + var importContext = importContextFactory.Create(); + + importContext.OnImportProgress += OnImportStatusUpdate; - foreach (var game in games) + foreach (var game in remoteLibrary) { - using var transaction = await databaseContext.Database.BeginTransactionAsync(); - try { - var remoteGame = await client.Games.GetAsync(game.Id); + var manifest = await gameClient.GetManifestAsync(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); - - await libraryService.AddToLibraryAsync(importedGame); - } - - await databaseContext.SaveChangesAsync(); - await transaction.CommitAsync(); + await importContext.AddAsync(manifest); } catch (Exception ex) { - await transaction.RollbackAsync(); - - Logger?.LogError(ex, "Could not import game {GameTitle}", game.Name); - } - finally - { - _importProgress.Index++; + Logger?.LogError(ex, "Could not add game with ID {GameId} to import queue", game.Id); } } - _importProgress.IsImporting = false; - if (OnImportComplete != null) - await OnImportComplete.Invoke(); - - Logger?.LogInformation("Importing games completed"); + await importContext.ImportQueueAsync(); } - - public async Task ImportGamesAsync(params Guid[] ids) - { - var games = await client.Library.GetAsync(); - var toImport = new List(); - - 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 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.GetAsync(importMediaId); - - return await ImportMediaAsync(media, game); - } - - public async Task 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 localPath = client.Media.GetLocalPath(media.FileId, media.Crc32); - - 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.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(); - } - } } diff --git a/LANCommander.Launcher.Services/Models/LibraryFilter.cs b/LANCommander.Launcher.Services/Models/LibraryFilter.cs index ffd16e56..d3c01320 100644 --- a/LANCommander.Launcher.Services/Models/LibraryFilter.cs +++ b/LANCommander.Launcher.Services/Models/LibraryFilter.cs @@ -10,6 +10,7 @@ using System.Security.Policy; using System.Text; using System.Threading.Tasks; using LANCommander.Launcher.Settings.Enums; +using LANCommander.SDK.Enums; namespace LANCommander.Launcher.Models { @@ -114,7 +115,7 @@ namespace LANCommander.Launcher.Models if (SelectedOptions.Installed) items = items.Where(i => (i.DataItem as Game).Installed); - items = items.Where(i => (i.DataItem as Game).Type.ValueIsIn(Data.Enums.GameType.MainGame, Data.Enums.GameType.StandaloneExpansion, Data.Enums.GameType.StandaloneMod)); + items = items.Where(i => (i.DataItem as Game).Type.ValueIsIn(GameType.MainGame, GameType.StandaloneExpansion, GameType.StandaloneMod)); switch (SelectedOptions.SortBy) { diff --git a/LANCommander.Launcher.Services/PowerShell/ElevatedScriptInterceptor.cs b/LANCommander.Launcher.Services/PowerShell/ElevatedScriptInterceptor.cs index 7c9092dc..8f4bfc40 100644 --- a/LANCommander.Launcher.Services/PowerShell/ElevatedScriptInterceptor.cs +++ b/LANCommander.Launcher.Services/PowerShell/ElevatedScriptInterceptor.cs @@ -31,7 +31,7 @@ public class ElevatedScriptInterceptor : IScriptInterceptor if (script.RunAsAdmin && !isElevated) { - var manifest = script.Variables.GetValue("GameManifest"); + var manifest = script.Variables.GetValue("GameManifest"); var options = new RunScriptCommandLineOptions { diff --git a/LANCommander.Launcher.Services/_BaseDatabaseService.cs b/LANCommander.Launcher.Services/_BaseDatabaseService.cs index c8bc1240..f8f04727 100644 --- a/LANCommander.Launcher.Services/_BaseDatabaseService.cs +++ b/LANCommander.Launcher.Services/_BaseDatabaseService.cs @@ -26,15 +26,18 @@ namespace LANCommander.Launcher.Services return await Context.Set().FindAsync(id); } + public virtual async Task FirstOrDefaultAsync(Expression> predicate) + { + return await Context.Set().FirstOrDefaultAsync(predicate); + } + public virtual IQueryable Query(Expression> predicate) { return Context.Set().Where(predicate); } - public virtual bool Exists(Guid id) - { - return GetAsync(id) != null; - } + public virtual async Task ExistsAsync(Guid id) => await Context.Set().AnyAsync(x => x.Id == id); + public virtual async Task ExistsAsync(Expression> predicate) => await Context.Set().AnyAsync(predicate); public virtual async Task AddAsync(T entity) { diff --git a/LANCommander.Launcher/UI/Components/ActionSelectorDialog.razor b/LANCommander.Launcher/UI/Components/ActionSelectorDialog.razor index bd7d24e0..1ade4d99 100644 --- a/LANCommander.Launcher/UI/Components/ActionSelectorDialog.razor +++ b/LANCommander.Launcher/UI/Components/ActionSelectorDialog.razor @@ -4,14 +4,14 @@ @inherits FeedbackComponent @inject GameService GameService - + @context.Name @code { - void Run(Game game, SDK.Models.Action action) + void Run(Game game, SDK.Models.Manifest.Action action) { GameService.Run(game, action); diff --git a/LANCommander.Launcher/UI/Components/ImportHandler.razor b/LANCommander.Launcher/UI/Components/ImportHandler.razor index 18881af6..13602477 100644 --- a/LANCommander.Launcher/UI/Components/ImportHandler.razor +++ b/LANCommander.Launcher/UI/Components/ImportHandler.razor @@ -1,4 +1,5 @@ @using LANCommander.Launcher.Models +@using LANCommander.Launcher.Services.Import @implements IDisposable @inject ImportManagerService ImportManagerService @inject ImportService ImportService @@ -29,9 +30,9 @@ if (ImportService != null) { // Clear any existing handlers - ImportService.OnImportComplete -= OnImportCompleteHandler; - ImportService.OnImportFailed -= OnImportFailedHandler; - ImportService.OnImportUpdated -= OnImportUpdatedHandler; + //ImportService.OnImportComplete -= OnImportCompleteHandler; + //ImportService.OnImportFailed -= OnImportFailedHandler; + //ImportService.OnImportUpdated -= OnImportUpdatedHandler; } } @@ -42,14 +43,14 @@ Logger?.LogInformation("Starting import process"); // Clear any existing handlers - ImportService.OnImportComplete -= OnImportCompleteHandler; - ImportService.OnImportFailed -= OnImportFailedHandler; - ImportService.OnImportUpdated -= OnImportUpdatedHandler; + //ImportService.OnImportComplete -= OnImportCompleteHandler; + //ImportService.OnImportFailed -= OnImportFailedHandler; + //ImportService.OnImportUpdated -= OnImportUpdatedHandler; // Add our handlers - ImportService.OnImportComplete += OnImportCompleteHandler; - ImportService.OnImportFailed += OnImportFailedHandler; - ImportService.OnImportUpdated += OnImportUpdatedHandler; + //ImportService.OnImportComplete += OnImportCompleteHandler; + //ImportService.OnImportFailed += OnImportFailedHandler; + //ImportService.OnImportUpdated += OnImportUpdatedHandler; // Now start the import process Importing = true; diff --git a/LANCommander.Launcher/UI/Components/Library/LibraryItemContextMenu.razor b/LANCommander.Launcher/UI/Components/Library/LibraryItemContextMenu.razor index 88de751b..6d714b83 100644 --- a/LANCommander.Launcher/UI/Components/Library/LibraryItemContextMenu.razor +++ b/LANCommander.Launcher/UI/Components/Library/LibraryItemContextMenu.razor @@ -1,6 +1,7 @@ @using LANCommander.Launcher.Data.Models @using LANCommander.Launcher.Settings @using LANCommander.SDK +@using LANCommander.SDK.Enums @using LANCommander.SDK.Helpers @using LANCommander.SDK.Services @using Microsoft.Extensions.Options @@ -48,7 +49,7 @@ @LocalizationService.GetString("ManageSaves") - if (Settings.Value.Games.InstallDirectories.Length > 1 || Game.DependentGames.Any(g => g.Type == Data.Enums.GameType.Expansion || g.Type == Data.Enums.GameType.Mod)) + if (Settings.Value.Games.InstallDirectories.Length > 1 || Game.DependentGames.Any(g => g.Type == GameType.Expansion || g.Type == GameType.Mod)) { @LocalizationService.GetString("Modify") @@ -113,18 +114,18 @@ else [Parameter] public bool ShowPrimaryActions { get; set; } Data.Models.Game Game { get; set; } - IEnumerable GameActions { get; set; } = []; + IEnumerable GameActions { get; set; } = []; protected override async Task OnParametersSetAsync() { if (Model.DataItem is Game game) { Game = game; - List actions = []; + List actions = []; if (Game.Installed) { - var manifest = await ManifestHelper.ReadAsync(Game.InstallDirectory, Game.Id); + var manifest = await ManifestHelper.ReadAsync(Game.InstallDirectory, Game.Id); if (manifest != null) { @@ -137,7 +138,7 @@ else } } - async Task Run(Game game, SDK.Models.Action action) + async Task Run(Game game, SDK.Models.Manifest.Action action) { var task = GameService.Run(game, action); @@ -162,7 +163,7 @@ else async Task Install() { - if (Settings.Value.Games.InstallDirectories.Length > 1 || (Model.DataItem as Game).DependentGames.Any(g => g.Type == Data.Enums.GameType.Expansion || g.Type == Data.Enums.GameType.Mod)) + if (Settings.Value.Games.InstallDirectories.Length > 1 || (Model.DataItem as Game).DependentGames.Any(g => g.Type == GameType.Expansion || g.Type == GameType.Mod)) { var modalOptions = new ModalOptions() { @@ -221,7 +222,7 @@ else async Task Uninstall() { - var manifest = await ManifestHelper.ReadAsync(Game.InstallDirectory, Game.Id); + var manifest = await ManifestHelper.ReadAsync(Game.InstallDirectory, Game.Id); string message; if (manifest.SavePaths != null && manifest.SavePaths.Any()) @@ -254,7 +255,7 @@ else async Task RemoveFromLibrary() { var hasStandaloneDependent = Game.DependentGames? - .Where(g => g.Type != Data.Enums.GameType.Expansion && g.Type != Data.Enums.GameType.Mod) + .Where(g => g.Type != GameType.Expansion && g.Type != GameType.Mod) .Any(g => LibraryService.IsInLibrary(g.Id)) ?? false; if (!hasStandaloneDependent) diff --git a/LANCommander.Launcher/UI/Components/MediaImage.razor b/LANCommander.Launcher/UI/Components/MediaImage.razor index b186c108..c94e9cfa 100644 --- a/LANCommander.Launcher/UI/Components/MediaImage.razor +++ b/LANCommander.Launcher/UI/Components/MediaImage.razor @@ -30,8 +30,8 @@ try { var media = await MediaService.GetAsync(Id); - if (media == null && (Connected || OfflineMode)) - media = await ImportService.ImportMediaAsync(Id); + //if (media == null && (Connected || OfflineMode)) + // media = await ImportService.ImportMediaAsync(Id); UpdateMediaUrl(media); } diff --git a/LANCommander.Launcher/UI/Components/PlayButton.razor b/LANCommander.Launcher/UI/Components/PlayButton.razor index 11d56c66..8b8c9e4a 100644 --- a/LANCommander.Launcher/UI/Components/PlayButton.razor +++ b/LANCommander.Launcher/UI/Components/PlayButton.razor @@ -2,14 +2,14 @@ @using LANCommander.Launcher.Data.Models @using LANCommander.Launcher.Models @using System.Diagnostics -@using LANCommander.SDK.Helpers -@inject SDK.Client Client +@using LANCommander.SDK +@using LANCommander.SDK.Services @inject GameService GameService @inject LibraryService LibraryService -@inject InstallService InstallService +@inject GameClient GameClient +@inject LobbyClient LobbyClient @inject ModalService ModalService @inject MessageService MessageService -@inject MessageBusService MessageBusService @inject LocalizationService LocalizationService @if (State == PlayButtonState.Running) @@ -78,11 +78,11 @@ else async Task UpdateState() { - if (Client.Games.IsRunning(LibraryItem.Key)) + if (GameClient.IsRunning(LibraryItem.Key)) { await ChangeState(PlayButtonState.Running); } - else if (State == PlayButtonState.Running && !Client.Games.IsRunning(LibraryItem.Key)) + else if (State == PlayButtonState.Running && !GameClient.IsRunning(LibraryItem.Key)) { await ChangeState(PlayButtonState.Idle); } @@ -98,7 +98,7 @@ else { var game = LibraryItem.DataItem as Game; - var actions = await Client.Games.GetActionsAsync(game.InstallDirectory, game.Id); + var actions = await GameClient.GetActionsAsync(game.InstallDirectory, game.Id); await ChangeState(PlayButtonState.Starting); @@ -110,7 +110,7 @@ else return; } - var primaryActions = actions.Where(a => a.IsPrimaryAction && (LibraryService.IsInstalled(a.GameId) || a.GameId == Guid.Empty)); + var primaryActions = actions.Where(a => a.IsPrimaryAction && LibraryService.IsInstalled(game.Id)); if (primaryActions.Count() == 0) { @@ -142,7 +142,7 @@ else WrapClassName = "game-actions-dialog", AfterClose = async () => { - Client.Lobbies.ReleaseSteam(); + LobbyClient.ReleaseSteam(); await ChangeState(PlayButtonState.Idle); } @@ -162,7 +162,7 @@ else { await ChangeState(PlayButtonState.Stopping); - await Client.Games.Stop(LibraryItem.Key); + await GameClient.Stop(LibraryItem.Key); await ChangeState(PlayButtonState.Idle); } diff --git a/LANCommander.Launcher/UI/Components/RemoveFromLibraryDialog.razor b/LANCommander.Launcher/UI/Components/RemoveFromLibraryDialog.razor index 7a20e436..a60693a3 100644 --- a/LANCommander.Launcher/UI/Components/RemoveFromLibraryDialog.razor +++ b/LANCommander.Launcher/UI/Components/RemoveFromLibraryDialog.razor @@ -1,5 +1,6 @@ @using LANCommander.Launcher.Data.Models @using LANCommander.Launcher.Models +@using LANCommander.SDK.Enums @using LANCommander.SDK.Extensions @inherits FeedbackComponent @inject LibraryService LibraryService @@ -27,7 +28,7 @@ IconSelected="@IconType.Outline.Close" IconUnselected="@IconType.Fill.Pushpin" ButtonPropsSelected="@(new[] { ("Danger", (object)true) })" - DisableSelector="g => g.Type == Data.Enums.GameType.Mod || g.Type == Data.Enums.GameType.Expansion" + DisableSelector="g => g.Type == GameType.Mod || g.Type == GameType.Expansion" /> } @@ -51,7 +52,7 @@ Game = (Game)Options.DataItem; DependentGames = Game?.DependentGames.OrderBy(g => g.SortTitle ?? g.Title).ToList() ?? []; - SelectedDependentGames = DependentGames?.Where(g => g.Type == Data.Enums.GameType.Expansion || g.Type == Data.Enums.GameType.Mod) ?? []; + SelectedDependentGames = DependentGames?.Where(g => g.Type == GameType.Expansion || g.Type == GameType.Mod) ?? []; } async Task Close() diff --git a/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameDetails.razor b/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameDetails.razor index 54a24463..a549678c 100644 --- a/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameDetails.razor +++ b/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameDetails.razor @@ -1,5 +1,6 @@ @using LANCommander.Launcher.Data.Models @using LANCommander.Launcher.Models +@using LANCommander.SDK.Enums @using LANCommander.SDK.Extensions @namespace LANCommander.Launcher.UI.Components @inject LibraryService LibraryService @@ -232,7 +233,7 @@ await Hide(); - if (Client.Settings.CurrentValue.Games.InstallDirectories.Length > 1 || (game?.DependentGames?.Any(g => g.Type.IsIn(Data.Enums.GameType.Expansion, Data.Enums.GameType.Mod)) ?? false)) + if (Client.Settings.CurrentValue.Games.InstallDirectories.Length > 1 || (game?.DependentGames?.Any(g => g.Type.IsIn(GameType.Expansion, GameType.Mod)) ?? false)) { var modalOptions = new ModalOptions() { @@ -272,7 +273,7 @@ try { - await ImportService.ImportGameAsync(SelectedGame.Id); + // await ImportService.ImportGameAsync(SelectedGame.Id); await LibraryService.AddToLibraryAsync(SelectedGame.Id); await LibraryService.RefreshItemsAsync(); diff --git a/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameItem.razor b/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameItem.razor index c3ccbce8..56c8ee13 100644 --- a/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameItem.razor +++ b/LANCommander.Launcher/UI/Pages/Depot/Components/DepotGameItem.razor @@ -44,7 +44,7 @@ StateHasChanged(); await Task.Yield(); - await ImportService.ImportGameAsync(Item.Key); + // await ImportService.ImportGameAsync(Item.Key); await LibraryService.AddToLibraryAsync(Item.Key); await LibraryService.RefreshItemsAsync(); diff --git a/LANCommander.SDK/Models/GameInstallationArchive.cs b/LANCommander.SDK/Models/GameInstallationArchive.cs index 5dd6857e..32077842 100644 --- a/LANCommander.SDK/Models/GameInstallationArchive.cs +++ b/LANCommander.SDK/Models/GameInstallationArchive.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using LANCommander.SDK.Models; namespace LANCommander.SDK.Services; public class GameInstallationArchiveInfo { - public GameManifest Manifest { get; internal set; } + public Models.Manifest.Game Manifest { get; internal set; } public List Entries { get; private set; } = []; public List SavePaths { get; internal set; } } @@ -13,5 +14,5 @@ public class GameInstallationArchiveInfo public class GameInstallationArchiveEntries { public GameInstallationArchiveInfo BaseGame { get; internal set; } = new(); - public Dictionary DependentGames { get; private set; } = []; + public Dictionary Addons { get; private set; } = []; } diff --git a/LANCommander.SDK/Models/GameInstallationFileList.cs b/LANCommander.SDK/Models/GameInstallationFileList.cs index 6d0b4ea3..e12dd350 100644 --- a/LANCommander.SDK/Models/GameInstallationFileList.cs +++ b/LANCommander.SDK/Models/GameInstallationFileList.cs @@ -14,7 +14,7 @@ public class GameInstallationFileListEntry public Guid GameId { get; internal set; } - public GameManifest Manifest { get; internal set; } + public Models.Manifest.Game Manifest { get; internal set; } public List Files { get; private set; } = []; public void Merge(GameInstallationFileListEntry otherInfo) diff --git a/LANCommander.SDK/Models/GameManifest.cs b/LANCommander.SDK/Models/GameManifest.cs deleted file mode 100644 index 9ba97ea9..00000000 --- a/LANCommander.SDK/Models/GameManifest.cs +++ /dev/null @@ -1,56 +0,0 @@ -using LANCommander.SDK.Enums; -using LANCommander.SDK.Models; -using System; -using System.Collections.Generic; - -namespace LANCommander.SDK -{ - public class GameManifest : IKeyedModel - { - public Guid Id { get; set; } - public string Title { get; set; } - public string SortTitle { get; set; } - public string Description { get; set; } - public string Notes { get; set; } - public GameType Type { get; set; } - public DateTime ReleasedOn { get; set; } - public string Engine { get; set; } - public IEnumerable Genre { get; set; } - public IEnumerable Tags { get; set; } - public IEnumerable Platforms { get; set; } - public IEnumerable Publishers { get; set; } - public IEnumerable Developers { get; set; } - public IEnumerable Collections { get; set; } - public string Version { get; set; } - public IEnumerable Actions { get; set; } - public bool Singleplayer { get; set; } - public MultiplayerInfo LocalMultiplayer { get; set; } - public MultiplayerInfo LanMultiplayer { get; set; } - public MultiplayerInfo OnlineMultiplayer { get; set; } - public IEnumerable SavePaths { get; set; } - public IEnumerable Keys { get; set; } - public IEnumerable