2026-05-09 17:27:05 +02:00
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Logging ;
namespace ImageCatalog_2 ;
internal static class CommandLineOperationRunner
{
private static readonly HashSet < string > HeadlessMarkers = new ( StringComparer . OrdinalIgnoreCase )
{
"--operation" ,
"--op" ,
"--config" ,
"--headless" ,
"--cli"
} ;
public static bool IsHeadlessRequest ( string [ ] ? args )
{
return args ? . Any ( arg = > HeadlessMarkers . Contains ( arg ) | | arg . StartsWith ( "--operation=" , StringComparison . OrdinalIgnoreCase ) | | arg . StartsWith ( "--config=" , StringComparison . OrdinalIgnoreCase ) ) = = true ;
}
public static async Task < int > RunAsync ( IServiceProvider services , string [ ] args )
{
var logger = services . GetRequiredService < ILoggerFactory > ( ) . CreateLogger ( "CommandLine" ) ;
try
{
var options = Parse ( args ) ;
if ( options . ShowHelp )
{
WriteUsage ( ) ;
return 0 ;
}
if ( string . IsNullOrWhiteSpace ( options . ConfigPath ) )
{
throw new ArgumentException ( "Missing required --config <path> argument." ) ;
}
if ( string . IsNullOrWhiteSpace ( options . Operation ) )
{
throw new ArgumentException ( "Missing required --operation <image-processing|number-ai|face-ai|race-upload> argument." ) ;
}
if ( ! File . Exists ( options . ConfigPath ) )
{
throw new FileNotFoundException ( "Configuration file not found." , options . ConfigPath ) ;
}
var model = services . GetRequiredService < DataModel > ( ) ;
await model . LoadSettingsFromFileAsync ( options . ConfigPath ) . ConfigureAwait ( false ) ;
ApplyOverrides ( model , options ) ;
using var cancellationTokenSource = new CancellationTokenSource ( ) ;
Console . CancelKeyPress + = ( _ , eventArgs ) = >
{
eventArgs . Cancel = true ;
cancellationTokenSource . Cancel ( ) ;
} ;
logger . LogInformation ( "Running ImageCatalog operation {Operation} with config {ConfigPath}" , options . Operation , options . ConfigPath ) ;
switch ( NormalizeOperation ( options . Operation ) )
{
case "image-processing" :
await model . ProcessImages ( ) . ConfigureAwait ( false ) ;
break ;
case "number-ai" :
await model . RunNumberAiAsync ( cancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
break ;
case "face-ai" :
await model . RunFaceAiAsync ( cancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
break ;
case "race-upload" :
throw new NotSupportedException ( "race-upload is not available in headless mode yet because the upload workflow still lives in the Avalonia view layer." ) ;
default :
throw new ArgumentException ( $"Unknown operation: {options.Operation}" ) ;
}
logger . LogInformation ( "ImageCatalog operation {Operation} completed" , options . Operation ) ;
return 0 ;
}
catch ( OperationCanceledException )
{
logger . LogWarning ( "Command-line operation canceled." ) ;
return 130 ;
}
catch ( Exception ex )
{
logger . LogError ( ex , "Command-line operation failed." ) ;
return 1 ;
}
}
private static void ApplyOverrides ( DataModel model , CommandLineOptions options )
{
if ( ! string . IsNullOrWhiteSpace ( options . ModelsPath ) )
{
model . ModelsFolderPath = options . ModelsPath ;
}
if ( ! string . IsNullOrWhiteSpace ( options . CsvPath ) )
{
model . CsvOutputPath = options . CsvPath ;
}
if ( options . UseGpu . HasValue )
{
model . UseNumberAiGpu = options . UseGpu . Value ;
if ( model . FaceGpuOptionEnabled | | ! options . UseGpu . Value )
{
model . UseFaceGpu = options . UseGpu . Value ;
}
}
2026-05-09 17:53:15 +02:00
if ( options . IncludeThumbnails . HasValue )
{
model . IncludeNumberAiThumbnails = options . IncludeThumbnails . Value ;
}
2026-05-09 17:27:05 +02:00
}
private static string NormalizeOperation ( string operation )
{
return operation . Trim ( ) . ToLowerInvariant ( ) switch
{
"images" or "image" or "process-images" or "image-processing" = > "image-processing" ,
"ai" or "ocr" or "number" or "number-ai" = > "number-ai" ,
"face" or "face-ai" = > "face-ai" ,
"race" or "race-upload" = > "race-upload" ,
var normalized = > normalized
} ;
}
private static CommandLineOptions Parse ( string [ ] args )
{
var options = new CommandLineOptions ( ) ;
for ( var i = 0 ; i < args . Length ; i + + )
{
var arg = args [ i ] ;
switch ( arg . ToLowerInvariant ( ) )
{
case "--help" :
case "-h" :
case "/?" :
options . ShowHelp = true ;
break ;
case "--operation" :
case "--op" :
options . Operation = ReadValue ( args , ref i , arg ) ;
break ;
case "--config" :
options . ConfigPath = ReadValue ( args , ref i , arg ) ;
break ;
case "--models" :
options . ModelsPath = ReadValue ( args , ref i , arg ) ;
break ;
case "--csv" :
options . CsvPath = ReadValue ( args , ref i , arg ) ;
break ;
case "--gpu" :
options . UseGpu = true ;
break ;
case "--cpu" :
options . UseGpu = false ;
break ;
2026-05-09 17:53:15 +02:00
case "--include-thumbnails" :
case "--include-tn" :
options . IncludeThumbnails = true ;
break ;
case "--no-thumbnails" :
case "--no-tn" :
options . IncludeThumbnails = false ;
break ;
2026-05-09 17:27:05 +02:00
case "--headless" :
case "--cli" :
break ;
default :
ApplyInlineArgument ( options , arg ) ;
break ;
}
}
return options ;
}
private static void ApplyInlineArgument ( CommandLineOptions options , string arg )
{
var separatorIndex = arg . IndexOf ( '=' ) ;
if ( separatorIndex < 0 )
{
throw new ArgumentException ( $"Unknown argument: {arg}" ) ;
}
var name = arg [ . . separatorIndex ] . ToLowerInvariant ( ) ;
var value = arg [ ( separatorIndex + 1 ) . . ] ;
switch ( name )
{
case "--operation" :
case "--op" :
options . Operation = value ;
break ;
case "--config" :
options . ConfigPath = value ;
break ;
case "--models" :
options . ModelsPath = value ;
break ;
case "--csv" :
options . CsvPath = value ;
break ;
default :
throw new ArgumentException ( $"Unknown argument: {arg}" ) ;
}
}
private static string ReadValue ( string [ ] args , ref int index , string name )
{
if ( index + 1 > = args . Length )
{
throw new ArgumentException ( $"Missing value for {name}." ) ;
}
return args [ + + index ] ;
}
private static void WriteUsage ( )
{
2026-05-09 17:53:15 +02:00
Console . WriteLine ( "Usage: ImageCatalog --config <settings.xml> --operation <image-processing|number-ai|face-ai|race-upload> [--models <folder>] [--csv <path>] [--cpu|--gpu] [--include-thumbnails|--no-thumbnails]" ) ;
2026-05-09 17:27:05 +02:00
}
private sealed class CommandLineOptions
{
public bool ShowHelp { get ; set ; }
public string Operation { get ; set ; } = string . Empty ;
public string ConfigPath { get ; set ; } = string . Empty ;
public string ModelsPath { get ; set ; } = string . Empty ;
public string CsvPath { get ; set ; } = string . Empty ;
public bool? UseGpu { get ; set ; }
2026-05-09 17:53:15 +02:00
public bool? IncludeThumbnails { get ; set ; }
2026-05-09 17:27:05 +02:00
}
}