Mostly-functional selective importer

This commit is contained in:
Pat Hartl 2025-07-26 19:19:38 -05:00
parent c81630b18a
commit 1250315110
11 changed files with 217 additions and 81 deletions

View file

@ -24,6 +24,7 @@ public class ActionImporter(
{
var action = new Data.Models.Action
{
Name = record.Name,
Game = ImportContext.DataRecord as Data.Models.Game,
Path = record.Path,
WorkingDirectory = record.WorkingDirectory,

View file

@ -7,7 +7,8 @@ namespace LANCommander.Server.Services.Importers;
public class CollectionImporter(
IMapper mapper,
CollectionService collectionService) : BaseImporter<Collection, Data.Models.Collection>
CollectionService collectionService,
GameService gameService) : BaseImporter<Collection, Data.Models.Collection>
{
public override async Task<ImportItemInfo> GetImportInfoAsync(Collection record)
{
@ -54,15 +55,19 @@ public class CollectionImporter(
public override async Task<Data.Models.Collection> UpdateAsync(Collection record)
{
var existing = await collectionService.Include(c => c.Games).FirstOrDefaultAsync(c => c.Name == record.Name);
var game = ImportContext.DataRecord as Data.Models.Game;
try
{
if (existing.Games == null)
existing.Games = new List<Data.Models.Game>();
existing.Games.Add(ImportContext.DataRecord as Data.Models.Game);
existing = await collectionService.UpdateAsync(existing);
if (!existing.Games.Any(g => g.Id == game.Id))
{
existing.Games.Add(await gameService.GetAsync(game.Id));
existing = await collectionService.UpdateAsync(existing);
}
return existing;
}

View file

@ -7,7 +7,8 @@ namespace LANCommander.Server.Services.Importers;
public class DeveloperImporter(
IMapper mapper,
CompanyService companyService) : BaseImporter<Company, Data.Models.Company>
CompanyService companyService,
GameService gameService) : BaseImporter<Company, Data.Models.Company>
{
public override async Task<ImportItemInfo> GetImportInfoAsync(Company record)
{
@ -33,14 +34,16 @@ public class DeveloperImporter(
public override async Task<Data.Models.Company> AddAsync(Company record)
{
if (ImportContext.DataRecord is not Data.Models.Game game)
if (ImportContext.DataRecord is not Data.Models.Game)
throw new ImportSkippedException<Company>(record, $"Cannot import developers for a {ImportContext.DataRecord.GetType().Name}");
try
{
var game = ImportContext.DataRecord as Data.Models.Game;
var company = new Data.Models.Company
{
DevelopedGames = new List<Data.Models.Game>() { game },
DevelopedGames = [await gameService.GetAsync(game.Id)],
Name = record.Name,
};
@ -56,19 +59,24 @@ public class DeveloperImporter(
public override async Task<Data.Models.Company> UpdateAsync(Company record)
{
if (ImportContext.DataRecord is not Data.Models.Game game)
if (ImportContext.DataRecord is not Data.Models.Game)
throw new ImportSkippedException<Company>(record, $"Cannot import developers for a {ImportContext.DataRecord.GetType().Name}");
var existing = await companyService.Include(g => g.DevelopedGames).FirstOrDefaultAsync(c => c.Name == record.Name);
try
{
var game = ImportContext.DataRecord as Data.Models.Game;
if (existing.DevelopedGames == null)
existing.DevelopedGames = new List<Data.Models.Game>();
if (!existing.DevelopedGames.Any(g => g.Id == game.Id))
{
existing.DevelopedGames.Add(await gameService.GetAsync(game.Id));
existing.DevelopedGames.Add(game);
existing = await companyService.UpdateAsync(existing);
existing = await companyService.UpdateAsync(existing);
}
return existing;
}

View file

@ -7,7 +7,8 @@ namespace LANCommander.Server.Services.Importers;
public class EngineImporter(
IMapper mapper,
EngineService engineService) : BaseImporter<Engine, Data.Models.Engine>
EngineService engineService,
GameService gameService) : BaseImporter<Engine, Data.Models.Engine>
{
public override async Task<ImportItemInfo> GetImportInfoAsync(Engine record)
{
@ -54,15 +55,20 @@ public class EngineImporter(
public override async Task<Data.Models.Engine> UpdateAsync(Engine record)
{
var existing = await engineService.Include(g => g.Games).FirstOrDefaultAsync(c => c.Name == record.Name);
var game = ImportContext.DataRecord as Data.Models.Game;
try
{
if (existing.Games == null)
existing.Games = new List<Data.Models.Game>();
existing.Games.Add(ImportContext.DataRecord as Data.Models.Game);
existing = await engineService.UpdateAsync(existing);
if (!existing.Games.Any(g => g.Id == game.Id))
{
existing.Games.Add(await gameService.GetAsync(game.Id));
existing = await engineService.UpdateAsync(existing);
}
return existing;
}

View file

@ -32,7 +32,26 @@ public class GameImporter(
public override async Task<Data.Models.Game> AddAsync(Game record)
{
var game = mapper.Map<Data.Models.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,
DirectoryName = record.DirectoryName,
};
if (!String.IsNullOrWhiteSpace(record.CreatedBy))
game.CreatedBy = await userService.GetAsync(record.CreatedBy);
if (!String.IsNullOrWhiteSpace(record.UpdatedBy))
game.UpdatedBy = await userService.GetAsync(record.UpdatedBy);
try
{
@ -58,10 +77,14 @@ public class GameImporter(
existing.Singleplayer = record.Singleplayer;
existing.Type = record.Type;
existing.IGDBId = record.IGDBId;
existing.CreatedBy = await userService.GetAsync(record.CreatedBy);
existing.CreatedOn = record.CreatedOn;
existing.UpdatedBy = await userService.GetAsync(record.UpdatedBy);
existing.DirectoryName = record.DirectoryName;
if (!String.IsNullOrWhiteSpace(record.CreatedBy))
existing.CreatedBy = await userService.GetAsync(record.CreatedBy);
if (!String.IsNullOrWhiteSpace(record.UpdatedBy))
existing.UpdatedBy = await userService.GetAsync(record.UpdatedBy);
existing = await gameService.UpdateAsync(existing);

View file

@ -7,7 +7,8 @@ namespace LANCommander.Server.Services.Importers;
public class GenreImporter(
IMapper mapper,
GenreService genreService) : BaseImporter<Genre, Data.Models.Genre>
GenreService genreService,
GameService gameService) : BaseImporter<Genre, Data.Models.Genre>
{
public override async Task<ImportItemInfo> GetImportInfoAsync(Genre record)
{
@ -54,15 +55,19 @@ public class GenreImporter(
public override async Task<Data.Models.Genre> UpdateAsync(Genre record)
{
var existing = await genreService.Include(g => g.Games).FirstOrDefaultAsync(c => c.Name == record.Name);
var game = ImportContext.DataRecord as Data.Models.Game;
try
{
if (existing.Games == null)
existing.Games = new List<Data.Models.Game>();
existing.Games.Add(ImportContext.DataRecord as Data.Models.Game);
existing = await genreService.UpdateAsync(existing);
if (!existing.Games.Any(g => g.Id == game.Id))
{
existing.Games.Add(await gameService.GetAsync(game.Id));
existing = await genreService.UpdateAsync(existing);
}
return existing;
}

View file

@ -145,7 +145,10 @@ public class ImportContext(
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Collections, Collections).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.CustomFields, CustomFields).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Developers, Developers).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync([gameManifest.Engine], Engines).ToListAsync());
if (gameManifest.Engine != null)
importItemInfo.AddRange(await GetImportItemInfoAsync([gameManifest.Engine], Engines).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Genres, Genres).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Keys, Keys).ToListAsync());
importItemInfo.AddRange(await GetImportItemInfoAsync(gameManifest.Media, Media).ToListAsync());
@ -350,7 +353,7 @@ public class ImportContext(
if (importRecordFlags.HasFlag(ImportRecordFlags.Developers))
await AddToImportQueueAsync(ImportRecordFlags.Developers, gameManifest.Developers);
if (importRecordFlags.HasFlag(ImportRecordFlags.Engine))
if (importRecordFlags.HasFlag(ImportRecordFlags.Engine) && gameManifest.Engine != null)
await AddToImportQueueAsync(ImportRecordFlags.Engine, [gameManifest.Engine]);
if (importRecordFlags.HasFlag(ImportRecordFlags.Genres))
@ -377,13 +380,13 @@ public class ImportContext(
if (importRecordFlags.HasFlag(ImportRecordFlags.Saves))
await AddToImportQueueAsync(ImportRecordFlags.Saves, gameManifest.Saves);
if (importRecordFlags.HasFlag(ImportRecordFlags.Scripts))
if (importRecordFlags.HasFlag(ImportRecordFlags.SavePaths))
await AddToImportQueueAsync(ImportRecordFlags.SavePaths, gameManifest.SavePaths);
if (importRecordFlags.HasFlag(ImportRecordFlags.Scripts))
await AddToImportQueueAsync(ImportRecordFlags.Scripts, gameManifest.Scripts);
if (importRecordFlags.HasFlag(ImportRecordFlags.Archives))
if (importRecordFlags.HasFlag(ImportRecordFlags.Tags))
await AddToImportQueueAsync(ImportRecordFlags.Tags, gameManifest.Tags);
}

View file

@ -7,7 +7,8 @@ namespace LANCommander.Server.Services.Importers;
public class PlatformImporter(
IMapper mapper,
PlatformService platformService) : BaseImporter<Platform, Data.Models.Platform>
PlatformService platformService,
GameService gameService) : BaseImporter<Platform, Data.Models.Platform>
{
public override async Task<ImportItemInfo> GetImportInfoAsync(Platform record)
{
@ -54,15 +55,19 @@ public class PlatformImporter(
public override async Task<Data.Models.Platform> UpdateAsync(Platform record)
{
var existing = await platformService.Include(p => p.Games).FirstOrDefaultAsync(c => c.Name == record.Name);
var game = ImportContext.DataRecord as Data.Models.Game;
try
{
if (existing.Games == null)
existing.Games = new List<Data.Models.Game>();
existing.Games.Add(ImportContext.DataRecord as Data.Models.Game);
existing = await platformService.UpdateAsync(existing);
if (!existing.Games.Any(g => g.Id == game.Id))
{
existing.Games.Add(await gameService.GetAsync(game.Id));
existing = await platformService.UpdateAsync(existing);
}
return existing;
}

View file

@ -7,7 +7,8 @@ namespace LANCommander.Server.Services.Importers;
public class PublisherImporter(
IMapper mapper,
CompanyService companyService) : BaseImporter<Company, Data.Models.Company>
CompanyService companyService,
GameService gameService) : BaseImporter<Company, Data.Models.Company>
{
public override async Task<ImportItemInfo> GetImportInfoAsync(Company record)
{
@ -35,9 +36,11 @@ public class PublisherImporter(
{
try
{
var game = ImportContext.DataRecord as Data.Models.Game;
var company = new Data.Models.Company
{
PublishedGames = new List<Data.Models.Game>() { ImportContext.DataRecord as Data.Models.Game },
PublishedGames = [await gameService.GetAsync(game.Id)],
Name = record.Name,
};
@ -54,15 +57,19 @@ public class PublisherImporter(
public override async Task<Data.Models.Company> UpdateAsync(Company record)
{
var existing = await companyService.Include(g => g.PublishedGames).FirstOrDefaultAsync(c => c.Name == record.Name);
var game = ImportContext.DataRecord as Data.Models.Game;
try
{
if (existing.PublishedGames == null)
existing.PublishedGames = new List<Data.Models.Game>();
existing.PublishedGames.Add(ImportContext.DataRecord as Data.Models.Game);
if (!existing.PublishedGames.Any(g => g.Id == game.Id))
{
existing.PublishedGames.Add(await gameService.GetAsync(game.Id));
existing = await companyService.UpdateAsync(existing);
existing = await companyService.UpdateAsync(existing);
}
return existing;
}

View file

@ -6,7 +6,8 @@ namespace LANCommander.Server.Services.Importers;
public class TagImporter(
IMapper mapper,
TagService tagService) : BaseImporter<Tag, Data.Models.Tag>
TagService tagService,
GameService gameService) : BaseImporter<Tag, Data.Models.Tag>
{
public override async Task<ImportItemInfo> GetImportInfoAsync(Tag record)
{
@ -53,15 +54,19 @@ public class TagImporter(
public override async Task<Data.Models.Tag> UpdateAsync(Tag record)
{
var existing = await tagService.Include(t => t.Games).FirstOrDefaultAsync(c => c.Name == record.Name);
var game = ImportContext.DataRecord as Data.Models.Game;
try
{
if (existing.Games == null)
existing.Games = new List<Data.Models.Game>();
existing.Games.Add(ImportContext.DataRecord as Data.Models.Game);
existing = await tagService.UpdateAsync(existing);
if (!existing.Games.Any(g => g.Id == game.Id))
{
existing.Games.Add(await gameService.GetAsync(game.Id));
existing = await tagService.UpdateAsync(existing);
}
return existing;
}

View file

@ -8,39 +8,88 @@
@inject IMessageService MessageService
@inject ILogger<ImportUploadDialog> Logger
<Flex Vertical Gap="FlexGap.Small">
<ChunkUploader
@ref="ChunkUploader"
Accept=".lcx"
@bind-File="File"
@bind-Status="Status"
StorageLocationId="StorageLocation.Id"
OnUploadCompleted="OnUploadCompleted"
OnUploadError="OnUploadError">
<Text>
<p>Drag and Drop</p>
<p>or</p>
<p>
<Button Type="@ButtonType.Primary" Style="margin-top: 8px;">Browse</Button>
</p>
</Text>
<Hint>@Options.Hint</Hint>
</ChunkUploader>
@if (!Uploaded)
{
<Flex Vertical Gap="FlexGap.Small">
<ChunkUploader
@ref="ChunkUploader"
Accept=".lcx"
@bind-File="File"
@bind-Status="Status"
StorageLocationId="StorageLocation.Id"
OnUploadCompleted="OnUploadCompleted"
OnUploadError="OnUploadError">
<Text>
<p>Drag and Drop</p>
<p>or</p>
<p>
<Button Type="@ButtonType.Primary" Style="margin-top: 8px;">Browse</Button>
</p>
</Text>
<Hint>@Options.Hint</Hint>
</ChunkUploader>
<StorageLocationSelector @bind-Value="StorageLocation" Type="StorageLocationType.Archive"/>
</Flex>
<StorageLocationSelector @bind-Value="StorageLocation" Type="StorageLocationType.Archive"/>
</Flex>
<Flex Justify="FlexJustify.End" Gap="FlexGap.Small" Style="margin-top: 16px;">
<FilePickerButton
EntrySelectable="@(entry => !String.IsNullOrWhiteSpace(entry.Name) && entry.Name.ToLower().EndsWith(".lcx"))"
OnSelected="OnLocalFileSelected"
Root="@RootPath"
Disabled="@(Status != "")">
Use Local File
</FilePickerButton>
<Button OnClick="Upload" Disabled="@(File == null || Status != "")" Type="@ButtonType.Primary">Upload</Button>
<Button OnClick="Cancel">Cancel</Button>
</Flex>
}
else
{
<Collapse>
@foreach (var group in ImportItems.GroupBy(i => i.Flag))
{
<Panel ShowArrow="false" Key="@group.Key.ToString()">
<HeaderTemplate>
<Checkbox @bind-Checked="SelectedFlags[group.Key]" />
<Flex Direction="FlexDirection.Horizontal" Justify="FlexJustify.SpaceBetween">
<Text>@group.Key</Text>
<Flex Justify="FlexJustify.End" Gap="FlexGap.Small" Style="margin-top: 16px;">
<FilePickerButton
EntrySelectable="@(entry => !String.IsNullOrWhiteSpace(entry.Name) && entry.Name.ToLower().EndsWith(".lcx"))"
OnSelected="OnLocalFileSelected"
Root="@RootPath"
Disabled="@(Status != "")">
Use Local File
</FilePickerButton>
<Button OnClick="Import" Disabled="@(File == null || Status != "")" Type="@ButtonType.Primary">Upload</Button>
<Button OnClick="Cancel">Cancel</Button>
</Flex>
@{
var size = group.Sum(i => i.Size);
if (size > 0)
{
<ByteSize Value="size" />
}
}
</Flex>
</HeaderTemplate>
<ChildContent>
<AntList DataSource="group">
<ChildContent Context="item">
<ListItem>
<Flex Direction="FlexDirection.Horizontal" Justify="FlexJustify.SpaceBetween">
<Text>@item.Name</Text>
@if (item.Size > 0)
{
<ByteSize Value="item.Size"/>
}
</Flex>
</ListItem>
</ChildContent>
</AntList>
</ChildContent>
</Panel>
}
</Collapse>
<Flex Justify="FlexJustify.End" Gap="FlexGap.Small" Style="margin-top: 16px;">
<Button OnClick="Import" Type="@ButtonType.Primary">Import</Button>
<Button OnClick="Cancel">Cancel</Button>
</Flex>
}
@code {
IBrowserFile File;
@ -52,6 +101,7 @@
string RootPath = Path.GetPathRoot(Directory.GetCurrentDirectory());
bool Uploaded = false;
bool IsValid = false;
string Filename;
string Status;
@ -98,8 +148,22 @@
.Where(f => f.Value)
.Aggregate(ImportRecordFlags.None, (cur, f) => cur | f.Key);
await ImportContext.PrepareImportQueueAsync(flags);
await ImportContext.ImportQueueAsync();
try
{
await ImportContext.PrepareImportQueueAsync(flags);
await ImportContext.ImportQueueAsync();
MessageService.Success("Game successfully imported!");
}
catch (Exception ex)
{
MessageService.Error("Game could not be imported!");
Logger?.LogError(ex, "An unknown error occurred while trying to import");
}
await Clear();
ImportContext.Dispose();
await CloseFeedbackAsync();
}
async Task Clear()
@ -140,6 +204,8 @@
var archivePath = await ArchiveService.GetArchiveFileLocationAsync(objectKey.ToString());
ImportItems = await ImportContext.InitializeImportAsync(archivePath);
Uploaded = true;
await InvokeAsync(StateHasChanged);
}
@ -178,6 +244,8 @@
try
{
ImportItems = await ImportContext.InitializeImportAsync(path);
Uploaded = true;
await InvokeAsync(StateHasChanged);
}