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 = - @; -} - -