FeedsDiscoverCommand.java

package net.sasasin.sreader.cli;

import java.net.URI;
import java.util.List;
import java.util.concurrent.Callable;
import net.sasasin.sreader.domain.FullTextMethod;
import net.sasasin.sreader.service.FeedDiscoveryService;
import net.sasasin.sreader.service.FeedDiscoveryService.DiscoveryResult;
import org.springframework.stereotype.Component;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;

@Command(
    name = "discover",
    description = {
      "Discover RSS/Atom (and similar) feed URLs from a web site page using Playwright.",
      "Outputs one URL per line (urls format) or TOML fragment (toml format).",
      "Default --format urls, --method http (used only for the generated toml full_text_method)."
    },
    mixinStandardHelpOptions = true,
    usageHelpWidth = 100)
@Component
public class FeedsDiscoverCommand implements Callable<Integer> {

  private final FeedDiscoveryService feedDiscoveryService;

  @Spec private CommandSpec spec;

  @Option(
      names = "--site-url",
      paramLabel = "<WEB_SITE_URL>",
      description = "Site URL to scan for feeds (http/https)",
      required = true)
  private String siteUrl;

  @Option(
      names = "--format",
      paramLabel = "<urls|toml>",
      description = "Output format",
      defaultValue = "urls")
  private String format;

  @Option(
      names = "--method",
      paramLabel = "<METHOD>",
      description = "full_text_method value to embed when --format toml",
      defaultValue = "http",
      converter = FullTextMethodConverter.class)
  private FullTextMethod method;

  @Option(names = "--verbose", description = "Print discovery diagnostics to STDERR")
  private boolean verbose;

  public FeedsDiscoverCommand(FeedDiscoveryService feedDiscoveryService) {
    this.feedDiscoveryService = feedDiscoveryService;
  }

  @Override
  public Integer call() {
    try {
      URI uri = UrlValidator.validateHttpUrl(siteUrl, "--site-url", spec);

      String fmt = (format != null ? format : "urls").trim().toLowerCase();
      if (!"urls".equals(fmt) && !"toml".equals(fmt)) {
        throw new picocli.CommandLine.ParameterException(
            spec.commandLine(), "Invalid --format value, must be urls or toml: " + format);
      }

      DiscoveryResult result = feedDiscoveryService.discoverWithResult(uri);
      List<URI> discovered = result.feedUrls();

      if (verbose) {
        spec.commandLine().getErr().printf("siteUrl=%s%n", uri);
        spec.commandLine().getErr().printf("finalUrl=%s%n", result.finalUrl());
        spec.commandLine().getErr().printf("discovered=%d%n", discovered.size());
      }

      if ("toml".equals(fmt)) {
        String m = method != null ? method.value() : "http";
        for (URI u : discovered) {
          spec.commandLine().getOut().println("[[feeds]]");
          spec.commandLine().getOut().println("url = \"" + escape(u.toString()) + "\"");
          spec.commandLine().getOut().println("status = \"active\"");
          spec.commandLine().getOut().println("full_text_method = \"" + m + "\"");
          spec.commandLine().getOut().println();
        }
      } else {
        // urls
        for (URI u : discovered) {
          spec.commandLine().getOut().println(u);
        }
      }
      return 0;
    } catch (picocli.CommandLine.ParameterException pe) {
      throw pe;
    } catch (IllegalStateException e) {
      spec.commandLine().getErr().println(e.getMessage());
      return 5;
    } catch (Exception e) {
      spec.commandLine().getErr().println("Error: " + e.getMessage());
      return 1;
    }
  }

  private String escape(String s) {
    return s.replace("\\", "\\\\").replace("\"", "\\\"");
  }
}