From 56588c20f27083fe3cae2f6b699bd835755c624b Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Fri, 26 Jun 2026 21:06:03 +0100 Subject: [PATCH] feat: Prompt to manually link unmatched platforms in match-platforms After automatic matching, interactively let the operator search the local platform catalog, create a new platform, skip, or quit ('q') for any platform that couldn't be matched automatically. Skipped entirely when stdin is redirected to avoid hanging non-interactive runs. --- Marechai.Igdb/Services/PlatformMatcher.cs | 131 ++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/Marechai.Igdb/Services/PlatformMatcher.cs b/Marechai.Igdb/Services/PlatformMatcher.cs index cb7ca429..7ff5c95b 100644 --- a/Marechai.Igdb/Services/PlatformMatcher.cs +++ b/Marechai.Igdb/Services/PlatformMatcher.cs @@ -38,6 +38,8 @@ public class PlatformMatcher int matched = 0; int needsReview = 0; + var unresolved = new List(); + foreach(IgdbPlatform igdbPlatform in pending) { string normalizedInput = Normalize(igdbPlatform.Name); @@ -105,15 +107,144 @@ public class PlatformMatcher if(match != null) matched++; else + { needsReview++; + unresolved.Add(igdbPlatform); + } } if(!dryRun) await context.SaveChangesAsync(); + if(!dryRun && unresolved.Count > 0) + { + if(Console.IsInputRedirected) + Console.WriteLine( + $"\n{unresolved.Count} platform(s) could not be automatically matched. Run interactively (not redirected) to link them manually."); + else + (matched, needsReview) = await ResolveInteractivelyAsync(context, platforms, unresolved, matched, + needsReview); + } + return (matched, needsReview); } + /// Walks the platforms that automatic matching could not resolve, one at a time, letting the + /// operator search the existing local catalog, create a brand-new , skip the + /// current one, or quit (leaving any remaining platforms as for a + /// later run). + async Task<(int matched, int needsReview)> ResolveInteractivelyAsync(MarechaiContext context, + List platforms, List unresolved, int matched, int needsReview) + { + Console.WriteLine( + $"\n{unresolved.Count} platform(s) could not be automatically matched. Resolve them now, or press 'q' at any prompt to stop and leave the rest unresolved."); + + foreach(IgdbPlatform igdbPlatform in unresolved) + { + (SoftwarePlatform resolved, bool quit) = PromptForPlatform(igdbPlatform, platforms); + + if(quit) + { + Console.WriteLine(" Quitting; remaining platforms left as \"needs review\"."); + + break; + } + + if(resolved == null) + continue; + + if(resolved.Id == 0) + { + context.SoftwarePlatforms.Add(resolved); + await context.SaveChangesAsync(); + platforms.Add(resolved); + Console.WriteLine($" Created new platform \"{resolved.Name}\" (id {resolved.Id})."); + } + + igdbPlatform.SoftwarePlatformId = resolved.Id; + igdbPlatform.MatchStatus = IgdbMatchStatus.Matched; + igdbPlatform.MatchType = "manual"; + igdbPlatform.MatchedOn = DateTime.UtcNow; + + matched++; + needsReview--; + + await context.SaveChangesAsync(); + Console.WriteLine($" Linked \"{igdbPlatform.Name}\" to \"{resolved.Name}\" (id {resolved.Id})."); + } + + return (matched, needsReview); + } + + /// Prompts for a single platform. Returns (null, true) on quit, (null, false) on + /// skip, or the chosen/newly-built (unsaved if newly built, signaled by + /// being 0) otherwise. + static (SoftwarePlatform platform, bool quit) PromptForPlatform(IgdbPlatform igdbPlatform, + List platforms) + { + while(true) + { + Console.WriteLine(); + Console.WriteLine($"Unmatched IGDB platform: \"{igdbPlatform.Name}\" (igdb id {igdbPlatform.Id})."); + Console.Write( + " Enter part of the local platform name to search, 'n' to create a new platform with this name, 's' to skip, or 'q' to quit: "); + + string input = Console.ReadLine()?.Trim(); + + if(input == null) + return (null, true); + + if(input.Length == 0) + continue; + + if(input.Equals("q", StringComparison.OrdinalIgnoreCase)) + return (null, true); + + if(input.Equals("s", StringComparison.OrdinalIgnoreCase)) + return (null, false); + + if(input.Equals("n", StringComparison.OrdinalIgnoreCase)) + { + Console.Write($" Create new platform named \"{igdbPlatform.Name}\"? [Y/n/q]: "); + string confirm = Console.ReadLine()?.Trim(); + + if(confirm != null && confirm.Equals("q", StringComparison.OrdinalIgnoreCase)) + return (null, true); + + if(confirm == null || confirm.Length == 0 || confirm.Equals("y", StringComparison.OrdinalIgnoreCase)) + return (new SoftwarePlatform { Name = igdbPlatform.Name }, false); + + continue; + } + + List candidates = platforms + .Where(p => p.Name.Contains(input, StringComparison.OrdinalIgnoreCase)) + .OrderBy(p => p.Name).ToList(); + + if(candidates.Count == 0) + { + Console.WriteLine(" No local platforms match that text. Try again."); + + continue; + } + + for(int i = 0; i < candidates.Count; i++) + Console.WriteLine($" [{i + 1}] {candidates[i].Name}"); + + Console.Write($" Pick a number (1-{candidates.Count}), 'q' to quit, or anything else to search again: "); + string choice = Console.ReadLine()?.Trim(); + + if(choice == null) + return (null, true); + + if(choice.Equals("q", StringComparison.OrdinalIgnoreCase)) + return (null, true); + + if(int.TryParse(choice, out int idx) && idx >= 1 && idx <= candidates.Count) + return (candidates[idx - 1], false); + } + } + static List Variants(SoftwarePlatform platform) { var variants = new List { platform.Name };