From 45aaa0ea8dc64bbd1a8d34b9e017c65890b9952a Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Mon, 1 Jun 2026 02:15:53 -0500 Subject: [PATCH] Simplify game addition w/ metadata lookup Adding a game now immediately provides a dialog to lookup a game through metadata providers. Selecting a result will create the game with the populated metadata. An additional option of automatically downloading art (icon, cover, background, logo) is also provided, though may be inaccurate and completely up to the results of the provider. --- .../Abstractions/IMediaGrabberService.cs | 7 + .../CompositeMediaGrabberService.cs | 13 + .../MetadataService.cs | 2 +- .../Providers/Metadata/HqMetadataProvider.cs | 1 + .../Providers/Metadata/IMetadataProvider.cs | 1 + .../Metadata/IgdbMetadataProvider.cs | 2 + .../Models/MediaSettings.cs | 1 + .../Models/AddGameDialogOptions.cs | 4 + .../Games/Components/AddGameDialog.razor | 102 ++++ .../Games/Components/GameMetadataLookup.razor | 472 +--------------- .../Components/MetadataLookupPanel.razor | 508 ++++++++++++++++++ .../UI/Pages/Games/Index.razor | 29 +- 12 files changed, 686 insertions(+), 456 deletions(-) create mode 100644 LANCommander.Server/Models/AddGameDialogOptions.cs create mode 100644 LANCommander.Server/UI/Pages/Games/Components/AddGameDialog.razor create mode 100644 LANCommander.Server/UI/Pages/Games/Components/MetadataLookupPanel.razor diff --git a/LANCommander.Server.Services/Abstractions/IMediaGrabberService.cs b/LANCommander.Server.Services/Abstractions/IMediaGrabberService.cs index 7bc992a8..83a098f3 100644 --- a/LANCommander.Server.Services/Abstractions/IMediaGrabberService.cs +++ b/LANCommander.Server.Services/Abstractions/IMediaGrabberService.cs @@ -14,11 +14,18 @@ namespace LANCommander.Server.Services.Abstractions Task DownloadAsync(MediaGrabberResult result, IProgress? progress) => DownloadAsync(result); + IEnumerable GetGrabberNames() => [Name]; + async IAsyncEnumerable> SearchStreamAsync( MediaType type, string keywords, [EnumeratorCancellation] CancellationToken cancellationToken = default) { yield return await SearchAsync(type, keywords); } + + IAsyncEnumerable> SearchStreamAsync( + MediaType type, string keywords, string? grabberName, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + => SearchStreamAsync(type, keywords, cancellationToken); } } diff --git a/LANCommander.Server.Services/MediaGrabbers/CompositeMediaGrabberService.cs b/LANCommander.Server.Services/MediaGrabbers/CompositeMediaGrabberService.cs index e45ac7c7..6245e0b5 100644 --- a/LANCommander.Server.Services/MediaGrabbers/CompositeMediaGrabberService.cs +++ b/LANCommander.Server.Services/MediaGrabbers/CompositeMediaGrabberService.cs @@ -32,12 +32,25 @@ namespace LANCommander.Server.Services.MediaGrabbers return results; } + public IEnumerable GetGrabberNames() => _grabbers.Select(g => g.Name); + public async IAsyncEnumerable> SearchStreamAsync( MediaType type, string keywords, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var batch in SearchStreamAsync(type, keywords, null, cancellationToken)) + yield return batch; + } + + public async IAsyncEnumerable> SearchStreamAsync( + MediaType type, string keywords, string? grabberName, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { var applicable = _grabbers.Where(g => g.SupportedMediaTypes.Contains(type)).ToList(); + if (!string.IsNullOrEmpty(grabberName)) + applicable = applicable.Where(g => g.Name == grabberName).ToList(); + if (applicable.Count == 0) yield break; diff --git a/LANCommander.Server.Services/MetadataService.cs b/LANCommander.Server.Services/MetadataService.cs index e8d961ca..7ea09c19 100644 --- a/LANCommander.Server.Services/MetadataService.cs +++ b/LANCommander.Server.Services/MetadataService.cs @@ -9,7 +9,7 @@ public class MetadataService(IServiceProvider serviceProvider) { var providers = serviceProvider.GetServices(); - return providers.Select(p => p.ProviderName); + return providers.Where(p => p.IsAvailable).Select(p => p.ProviderName); } public IMetadataProvider? GetProvider(string providerName) diff --git a/LANCommander.Server.Services/Providers/Metadata/HqMetadataProvider.cs b/LANCommander.Server.Services/Providers/Metadata/HqMetadataProvider.cs index b5551f26..87f28de8 100644 --- a/LANCommander.Server.Services/Providers/Metadata/HqMetadataProvider.cs +++ b/LANCommander.Server.Services/Providers/Metadata/HqMetadataProvider.cs @@ -14,6 +14,7 @@ public class HqMetadataProvider( private IReadOnlyList? _cachedProviders; public string ProviderName => "LANCommander HQ"; + public bool IsAvailable => settingsProvider.CurrentValue.Server.HQ.IsAuthenticated; public async Task?> GetSubProvidersAsync() { diff --git a/LANCommander.Server.Services/Providers/Metadata/IMetadataProvider.cs b/LANCommander.Server.Services/Providers/Metadata/IMetadataProvider.cs index 63870d4a..2dfd56d7 100644 --- a/LANCommander.Server.Services/Providers/Metadata/IMetadataProvider.cs +++ b/LANCommander.Server.Services/Providers/Metadata/IMetadataProvider.cs @@ -5,6 +5,7 @@ namespace LANCommander.Server.Services.Providers.Metadata; public interface IMetadataProvider { public string ProviderName { get; } + bool IsAvailable => true; public Task?> SearchGamesAsync(string input, int limit = 10, int offset = 0); public Task GetGameAsync(string gameId); diff --git a/LANCommander.Server.Services/Providers/Metadata/IgdbMetadataProvider.cs b/LANCommander.Server.Services/Providers/Metadata/IgdbMetadataProvider.cs index dccd2363..5b182a71 100644 --- a/LANCommander.Server.Services/Providers/Metadata/IgdbMetadataProvider.cs +++ b/LANCommander.Server.Services/Providers/Metadata/IgdbMetadataProvider.cs @@ -25,6 +25,8 @@ public class IgdbMetadataProvider( } public string ProviderName => "IGDB"; + public bool IsAvailable => !string.IsNullOrWhiteSpace(settingsProvider.CurrentValue.Server.IGDB.ClientId) + && !string.IsNullOrWhiteSpace(settingsProvider.CurrentValue.Server.IGDB.ClientSecret); public async Task?> SearchGamesAsync(string input, int limit = 10, int offset = 0) { diff --git a/LANCommander.Server.Settings/Models/MediaSettings.cs b/LANCommander.Server.Settings/Models/MediaSettings.cs index 636f1c4b..4b12bcf1 100644 --- a/LANCommander.Server.Settings/Models/MediaSettings.cs +++ b/LANCommander.Server.Settings/Models/MediaSettings.cs @@ -6,6 +6,7 @@ namespace LANCommander.Server.Settings.Models; public class MediaSettings { public string SteamGridDbApiKey { get; set; } = String.Empty; + public bool AutoDownloadMedia { get; set; } = false; public IEnumerable MediaTypes { get; set; } = [ diff --git a/LANCommander.Server/Models/AddGameDialogOptions.cs b/LANCommander.Server/Models/AddGameDialogOptions.cs new file mode 100644 index 00000000..c290b046 --- /dev/null +++ b/LANCommander.Server/Models/AddGameDialogOptions.cs @@ -0,0 +1,4 @@ +namespace LANCommander.Server.Models +{ + public class AddGameDialogOptions { } +} diff --git a/LANCommander.Server/UI/Pages/Games/Components/AddGameDialog.razor b/LANCommander.Server/UI/Pages/Games/Components/AddGameDialog.razor new file mode 100644 index 00000000..6a1cdb6f --- /dev/null +++ b/LANCommander.Server/UI/Pages/Games/Components/AddGameDialog.razor @@ -0,0 +1,102 @@ +@using LANCommander.Server.Services.Abstractions +@using LANCommander.Server.Services.Models +@using LANCommander.Server.Settings +@inherits FeedbackComponent +@inject IMediaGrabberService MediaGrabberService +@inject MediaService MediaService +@inject GameService GameService +@inject SettingsProvider SettingsProvider +@inject NavigationManager NavigationManager +@inject ILogger Logger + + + Automatically download art + + +@code { + MetadataLookupPanel _panel; + bool _autoDownloadMedia; + + static readonly MediaType[] AutoDownloadTypes = + [ + MediaType.Icon, + MediaType.Cover, + MediaType.Background, + MediaType.Logo, + MediaType.Grid + ]; + + protected override void OnInitialized() + { + _autoDownloadMedia = SettingsProvider.CurrentValue.Server.Media.AutoDownloadMedia; + } + + private void OnAutoDownloadMediaChanged(bool value) + { + _autoDownloadMedia = value; + SettingsProvider.Update(s => s.Server.Media.AutoDownloadMedia = value); + } + + private async Task OnGameImported(Guid gameId) + { + if (_autoDownloadMedia) + { + await DownloadMediaForGame(gameId); + } + + await CloseFeedbackAsync(); + NavigationManager.NavigateTo($"/Games/{gameId}"); + } + + private async Task DownloadMediaForGame(Guid gameId) + { + try + { + var game = await GameService.GetAsync(gameId); + + if (game == null) + return; + + foreach (var mediaType in AutoDownloadTypes) + { + try + { + await _panel.UpdateStatusAsync($"Downloading {mediaType}..."); + + var results = await MediaGrabberService.SearchAsync(mediaType, game.Title); + var result = results.FirstOrDefault(); + + if (result == null) + continue; + + using var download = await MediaGrabberService.DownloadAsync(result); + + var media = new Media + { + GameId = gameId, + Type = mediaType, + SourceUrl = result.SourceUrl, + MimeType = download.MimeType, + StorageLocation = await MediaService.GetDefaultStorageLocationAsync(), + Crc32 = string.Empty + }; + + await MediaService.WriteToFileAsync(media, download.Stream); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to auto-download {MediaType} for game {GameId}", mediaType, gameId); + } + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to auto-download media for game {GameId}", gameId); + } + } +} diff --git a/LANCommander.Server/UI/Pages/Games/Components/GameMetadataLookup.razor b/LANCommander.Server/UI/Pages/Games/Components/GameMetadataLookup.razor index d75fb8b7..201ea7b7 100644 --- a/LANCommander.Server/UI/Pages/Games/Components/GameMetadataLookup.razor +++ b/LANCommander.Server/UI/Pages/Games/Components/GameMetadataLookup.razor @@ -1,488 +1,54 @@ -@using AntDesign.TableModels -@using LANCommander.Server.ImportExport.Factories -@using LANCommander.Server.Services.Providers.Metadata @inject MetadataService MetadataService -@inject GameService GameService -@inject ImportContextFactory ImportContextFactory -@inject IMessageService MessageService -@inject ILogger Logger -@{ - RenderFragment SearchFooter = - @; - - RenderFragment MergeFooter = - @; -} - -