From c26d9cfabbc5f6415e2508ebfed28351536dfedf Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 4 Jul 2026 02:35:09 -0500 Subject: [PATCH] Add endpoint to list full game info for user library --- .../Endpoints/LibraryEndpoints.cs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/LANCommander.Server/Endpoints/LibraryEndpoints.cs b/LANCommander.Server/Endpoints/LibraryEndpoints.cs index 50f9a4fe..97d473ce 100644 --- a/LANCommander.Server/Endpoints/LibraryEndpoints.cs +++ b/LANCommander.Server/Endpoints/LibraryEndpoints.cs @@ -16,6 +16,7 @@ public static class LibraryEndpoints var group = routes.MapGroup("/api/Library").RequireAuthorization(); group.MapGet("/", GetAsync); + group.MapGet("/Games", GetGamesAsync); group.MapPost("/AddToLibrary/{id:guid}", AddToLibraryAsync); group.MapPost("/RemoveFromLibrary/{id:guid}", RemoveFromLibraryAsync); group.MapPost("/RemoveFromLibrary/{id:guid}/Addons", RemoveFromLibraryWithAddonsAsync); @@ -99,6 +100,100 @@ public static class LibraryEndpoints } } + internal static async Task GetGamesAsync( + ClaimsPrincipal userPrincipal, + [FromServices] IMapper mapper, + [FromServices] IFusionCache cache, + [FromServices] GameService gameService, + [FromServices] LibraryService libraryService, + [FromServices] UserService userService, + [FromServices] SettingsProvider settingsProvider, + [FromServices] ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger("LibraryApi"); + + try + { + if (userPrincipal.Identity?.Name is null) + { + logger.LogError("Failed to get user library games: User is not authenticated"); + return TypedResults.Ok(Enumerable.Empty()); + } + + var user = await userService.GetAsync(userPrincipal.Identity.Name); + + // When user libraries are disabled the "library" is the full catalog. Cache it under a + // shared key since it is identical for every user. + if (!settingsProvider.CurrentValue.Server.Library.EnableUserLibraries) + { + logger.LogDebug("User libraries are disabled; returning all games with media"); + + var allGames = await cache.GetOrSetAsync( + "Library/Games", + async _ => mapper.Map>(await LoadGamesWithMediaAsync(gameService, null)), + TimeSpan.MaxValue, + tags: ["Library", "Games"]); + + return TypedResults.Ok(allGames); + } + + logger.LogDebug("Getting library games for user {User}", userPrincipal.Identity.Name); + + var result = await cache.GetOrSetAsync( + $"Library/{user.Id}/Games", + async _ => + { + var library = await libraryService.GetByUserIdAsync(user.Id); + var libraryGameIds = library.Games.Select(g => g.Id).ToList(); + + var games = await LoadGamesWithMediaAsync(gameService, libraryGameIds); + + var mapped = mapper.Map>(games).ToList(); + + foreach (var game in mapped) + game.InLibrary = true; + + return mapped; + }, + TimeSpan.MaxValue, + // Tagged with Library/{userId} so LibraryService add/remove invalidates it automatically. + tags: ["Library", "Games", $"Library/{user.Id}"]); + + return TypedResults.Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to get user library games for user {User}", userPrincipal.Identity?.Name ?? "Unknown User"); + return TypedResults.Ok(Enumerable.Empty()); + } + } + + // Loads library games with just the navigations the launcher needs to render cards and the + // detail view (cover/icon/logo/hero live in Media). Pass null for libraryGameIds to load all games. + private static async Task> LoadGamesWithMediaAsync( + GameService gameService, + IReadOnlyCollection? libraryGameIds) + { + var query = gameService + .Include(g => g.Collections) + .Include(g => g.Developers) + .Include(g => g.Engine) + .Include(g => g.Genres) + .Include(g => g.Media) + .Include(g => g.MultiplayerModes) + .Include(g => g.Platforms) + .Include(g => g.Publishers) + .Include(g => g.Tags) + .AsNoTracking() + .AsSplitQuery(); + + var games = libraryGameIds is null + ? await query.GetAsync() + : await query.GetAsync(g => libraryGameIds.Contains(g.Id)); + + return games.OrderByTitle(g => string.IsNullOrWhiteSpace(g.SortTitle) ? g.Title : g.SortTitle); + } + internal static async Task AddToLibraryAsync( Guid id, ClaimsPrincipal userPrincipal,