From b657bd29976b387eec47dc8d38c5d086a3c57488 Mon Sep 17 00:00:00 2001 From: Cyprian Zdebski Date: Sat, 13 Dec 2025 23:42:56 +0100 Subject: [PATCH 1/2] add CLI mode --- .../com/penguinpush/cullergrader/CLI.java | 281 ++++++++++++++++++ .../com/penguinpush/cullergrader/Main.java | 43 ++- 2 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/penguinpush/cullergrader/CLI.java diff --git a/src/main/java/com/penguinpush/cullergrader/CLI.java b/src/main/java/com/penguinpush/cullergrader/CLI.java new file mode 100644 index 0000000..20895c7 --- /dev/null +++ b/src/main/java/com/penguinpush/cullergrader/CLI.java @@ -0,0 +1,281 @@ +package com.penguinpush.cullergrader; + +import com.penguinpush.cullergrader.logic.*; +import com.penguinpush.cullergrader.media.*; +import com.penguinpush.cullergrader.config.AppConstants; + +import java.io.File; +import java.util.List; + +/** + * Command-line interface for Cullergrader. + * Provides photo grouping and export functionality without launching the GUI. + */ +public class CLI { + + // Exit codes + private static final int EXIT_SUCCESS = 0; + private static final int EXIT_FAILURE = 1; + + // Parsed arguments with defaults from AppConstants + private String inputPath = null; + private String outputPath = null; + private float timeThreshold = AppConstants.TIME_THRESHOLD_SECONDS; + private float similarityThreshold = AppConstants.SIMILARITY_THRESHOLD_PERCENT; + + /** + * Main entry point for CLI mode. + * + * @param args Command-line arguments + * @return Exit code (0 = success, 1 = failure) + */ + public int run(String[] args) { + // Handle --help first + if (hasArgument(args, "--help") || hasArgument(args, "-h")) { + printHelp(); + return EXIT_SUCCESS; + } + + // Parse arguments + if (!parseArguments(args)) { + System.err.println("Error: Invalid arguments. Use --help for usage information."); + return EXIT_FAILURE; + } + + // Validate required arguments + if (inputPath == null) { + System.err.println("Error: --input is required."); + printHelp(); + return EXIT_FAILURE; + } + + // Validate input directory + File inputFolder = new File(inputPath); + if (!inputFolder.exists() || !inputFolder.isDirectory()) { + System.err.println("Error: Input directory does not exist: " + inputPath); + return EXIT_FAILURE; + } + + if (!inputFolder.canRead()) { + System.err.println("Error: Cannot read input directory: " + inputPath); + return EXIT_FAILURE; + } + + // Validate output directory if provided + File outputFolder = null; + if (outputPath != null) { + outputFolder = new File(outputPath); + if (outputFolder.exists() && !outputFolder.isDirectory()) { + System.err.println("Error: Output path exists but is not a directory: " + outputPath); + return EXIT_FAILURE; + } + + if (outputFolder.exists() && !outputFolder.canWrite()) { + System.err.println("Error: Cannot write to output directory: " + outputPath); + return EXIT_FAILURE; + } + } + + // Execute workflow + try { + executeWorkflow(inputFolder, outputFolder); + return EXIT_SUCCESS; + } catch (Exception e) { + System.err.println("Error: Processing failed - " + e.getMessage()); + e.printStackTrace(); + return EXIT_FAILURE; + } + } + + /** + * Parses command-line arguments. + * + * @param args Command-line arguments + * @return true if parsing succeeded, false on error + */ + private boolean parseArguments(String[] args) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + + // Input path + if (arg.equals("--input") || arg.equals("-i")) { + if (i + 1 >= args.length) { + System.err.println("Error: --input requires a value"); + return false; + } + inputPath = args[++i]; + } + // Output path + else if (arg.equals("--output") || arg.equals("-o")) { + if (i + 1 >= args.length) { + System.err.println("Error: --output requires a value"); + return false; + } + outputPath = args[++i]; + } + // Time threshold + else if (arg.equals("--time") || arg.equals("-t")) { + if (i + 1 >= args.length) { + System.err.println("Error: --time requires a value"); + return false; + } + try { + timeThreshold = Float.parseFloat(args[++i]); + if (timeThreshold <= 0) { + System.err.println("Error: Time threshold must be positive"); + return false; + } + } catch (NumberFormatException e) { + System.err.println("Error: Invalid time threshold value: " + args[i]); + return false; + } + } + // Similarity threshold + else if (arg.equals("--similarity") || arg.equals("-s")) { + if (i + 1 >= args.length) { + System.err.println("Error: --similarity requires a value"); + return false; + } + try { + similarityThreshold = Float.parseFloat(args[++i]); + if (similarityThreshold < 0 || similarityThreshold > 100) { + System.err.println("Error: Similarity threshold must be 0-100"); + return false; + } + } catch (NumberFormatException e) { + System.err.println("Error: Invalid similarity threshold value: " + args[i]); + return false; + } + } + // Skip --help and -h (handled in run method) + else if (arg.equals("--help") || arg.equals("-h")) { + // Already handled in run(), just skip + } + // Unknown argument + else if (arg.startsWith("-")) { + System.err.println("Error: Unknown argument: " + arg); + return false; + } + } + + return true; + } + + /** + * Executes the main CLI workflow: load photos, generate groups, and export. + * + * @param inputFolder Input directory containing photos + * @param outputFolder Output directory for best takes + */ + private void executeWorkflow(File inputFolder, File outputFolder) { + long startTime = System.currentTimeMillis(); + boolean previewMode = (outputFolder == null); + + // Print configuration header + System.out.println("Cullergrader CLI"); + System.out.println("================"); + System.out.println("Input: " + inputFolder.getAbsolutePath()); + if (!previewMode) { + System.out.println("Output: " + outputFolder.getAbsolutePath()); + } else { + System.out.println("Mode: Preview (no files will be exported)"); + } + System.out.println("Time threshold: " + timeThreshold + " seconds"); + System.out.println("Similarity threshold: " + similarityThreshold + "%"); + System.out.println(); + + // Load and hash photos + System.out.println("Loading and hashing photos from: " + inputFolder.getAbsolutePath()); + GroupingEngine engine = new GroupingEngine(); + List photos = engine.photoListFromFolder(inputFolder); + + if (photos.isEmpty()) { + System.out.println("No photos found in input directory."); + return; + } + + System.out.println("Found " + photos.size() + " photos"); + System.out.println(); + + // Generate groups + System.out.println("Generating groups with thresholds: " + timeThreshold + "s time, " + similarityThreshold + "% similarity"); + List groups = engine.generateGroups(photos, timeThreshold, similarityThreshold); + + System.out.println("Created " + groups.size() + " groups from " + photos.size() + " photos"); + System.out.println(); + + // Export or preview + if (previewMode) { + System.out.println("Preview - Best takes that would be exported:"); + System.out.println("--------------------------------------------"); + for (int i = 0; i < groups.size(); i++) { + PhotoGroup group = groups.get(i); + Photo bestTake = group.getBestTake(); + if (bestTake != null) { + System.out.println("[Group " + i + "] " + bestTake.getFile().getName()); + } + } + System.out.println(); + System.out.println("To export these " + groups.size() + " files, run again with --output "); + } else { + System.out.println("Exporting best takes to: " + outputFolder.getAbsolutePath()); + FileUtils.exportBestTakes(groups, outputFolder); + System.out.println(); + System.out.println("Successfully exported " + groups.size() + " files"); + } + + // Summary + long endTime = System.currentTimeMillis(); + long durationMs = endTime - startTime; + double durationSec = durationMs / 1000.0; + System.out.println(); + System.out.println("Processing completed in " + String.format("%.2f", durationSec) + " seconds"); + } + + /** + * Prints help message showing usage and available options. + */ + private void printHelp() { + System.out.println("Cullergrader CLI - Photo grouping and export tool"); + System.out.println(); + System.out.println("USAGE:"); + System.out.println(" java -jar cullergrader.jar [OPTIONS]"); + System.out.println(); + System.out.println(" No arguments launches GUI mode"); + System.out.println(); + System.out.println("OPTIONS:"); + System.out.println(" -i, --input Input folder containing photos (required)"); + System.out.println(" -o, --output Output folder for best takes (optional, preview mode if omitted)"); + System.out.println(" -t, --time Time threshold in seconds (default: " + AppConstants.TIME_THRESHOLD_SECONDS + ")"); + System.out.println(" -s, --similarity Similarity threshold 0-100 (default: " + AppConstants.SIMILARITY_THRESHOLD_PERCENT + ")"); + System.out.println(" -h, --help Show this help message"); + System.out.println(); + System.out.println("EXAMPLES:"); + System.out.println(" # Preview mode (no export)"); + System.out.println(" java -jar cullergrader.jar --input /photos"); + System.out.println(); + System.out.println(" # Export mode"); + System.out.println(" java -jar cullergrader.jar --input /photos --output /export"); + System.out.println(); + System.out.println(" # Custom thresholds with export"); + System.out.println(" java -jar cullergrader.jar -i /photos -o /export -t 10 -s 40"); + System.out.println(); + } + + /** + * Helper method to check if a specific flag is present in arguments. + * Used by Main.java to detect CLI mode. + * + * @param args Command-line arguments + * @param flag Flag to search for + * @return true if flag is present, false otherwise + */ + public static boolean hasArgument(String[] args, String flag) { + for (String arg : args) { + if (arg.equals(flag)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/penguinpush/cullergrader/Main.java b/src/main/java/com/penguinpush/cullergrader/Main.java index bae4efa..f5eb719 100644 --- a/src/main/java/com/penguinpush/cullergrader/Main.java +++ b/src/main/java/com/penguinpush/cullergrader/Main.java @@ -10,17 +10,44 @@ public class Main { public static void main(String[] args) { - // load theme - if (AppConstants.DARK_THEME) { - FlatDarculaLaf.setup(); + // Detect CLI mode by checking for CLI-specific arguments + if (isCLIMode(args)) { + // CLI mode - skip GUI initialization + CLI cli = new CLI(); + int exitCode = cli.run(args); + System.exit(exitCode); } else { - FlatIntelliJLaf.setup(); + // GUI mode + // load theme + if (AppConstants.DARK_THEME) { + FlatDarculaLaf.setup(); + } else { + FlatIntelliJLaf.setup(); + } + + GroupingEngine groupingEngine = new GroupingEngine(); + ImageLoader imageLoader = new ImageLoader(); + + SwingUtilities.invokeLater(() -> new GroupGridFrame(imageLoader, groupingEngine)); + GroupGridFrame.initializeLoggerCallback(); } + } - GroupingEngine groupingEngine = new GroupingEngine(); - ImageLoader imageLoader = new ImageLoader(); + /** + * Determines if the application should run in CLI mode based on command-line arguments. + * + * @param args Command-line arguments + * @return true if CLI mode should be used, false for GUI mode + */ + private static boolean isCLIMode(String[] args) { + if (args.length == 0) { + return false; + } - SwingUtilities.invokeLater(() -> new GroupGridFrame(imageLoader, groupingEngine)); - GroupGridFrame.initializeLoggerCallback(); + // Check for CLI-specific flags + return CLI.hasArgument(args, "--input") || + CLI.hasArgument(args, "-i") || + CLI.hasArgument(args, "--help") || + CLI.hasArgument(args, "-h"); } } From 9518e5c34fb39482d5ca7edd17efa7b0caac6771 Mon Sep 17 00:00:00 2001 From: Cyprian Zdebski Date: Sat, 13 Dec 2025 23:43:02 +0100 Subject: [PATCH 2/2] update README --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index 2f67e8e..12d08c9 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,47 @@ Best takes can be exported to a folder using `File > Export Best Takes` or with ![images/export_to.png](images/export_to.png) +## CLI Usage + +Cullergrader can be run in command-line mode for automated workflows and scripting. + +### Basic Usage + +```bash +# Launch GUI (no arguments) +java -jar cullergrader.jar + +# Run CLI mode +java -jar cullergrader.jar --input /path/to/photos --output /path/to/export +``` + +### CLI Options + +| Option | Short | Description | Required | +|--------|-------|-------------|----------| +| `--input` | `-i` | Input folder containing photos | Yes | +| `--output` | `-o` | Output folder for best takes (preview mode if omitted) | No | +| `--time` | `-t` | Time threshold in seconds (default: 15) | No | +| `--similarity` | `-s` | Similarity threshold 0-100 (default: 45) | No | +| `--help` | `-h` | Show help message | No | + +### Examples + +**Preview mode (no export)**: +```bash +java -jar cullergrader.jar --input ~/photos/vacation +``` + +**Export to folder**: +```bash +java -jar cullergrader.jar --input ~/photos/vacation --output ~/photos/best +``` + +**Custom thresholds**: +```bash +java -jar cullergrader.jar -i ~/photos/vacation -o ~/photos/best -t 10 -s 40 +``` + ## Config ### Default Config ```json