package pl.genschu.rexcatalog; import pl.genschu.rexcatalog.db.Database; import pl.genschu.rexcatalog.ingest.Ingestor; import java.io.File; import java.nio.file.Path; import java.sql.ResultSet; import java.sql.Statement; import java.util.ArrayList; import java.util.List; /** * CLI katalogu. Krok 1: indeksowanie obrazów do SQLite. * REST + MCP siadają później na tej samej bazie. */ public final class Main { private static final String CORE_VERSION = System.getProperty("catalog.coreVersion", "nieznana"); public static void main(String[] args) throws Exception { if (args.length == 0) { usage(); return; } Path dataDir = Path.of(System.getProperty("catalog.data", "data")); try { switch (args[0]) { case "ingest" -> ingest(dataDir, args); case "list" -> list(dataDir); case "stats" -> stats(dataDir); default -> usage(); } } finally { // bez tego proces wisi po zakończeniu pracy — patrz Headless.shutdown() Headless.shutdown(); } } private static void ingest(Path dataDir, String[] args) throws Exception { if (args.length < 2) { System.err.println("ingest wymaga co najmniej jednej ścieżki"); return; } Headless.boot(); List sources = new ArrayList<>(); for (int i = 1; i < args.length; i++) { sources.addAll(Ingestor.expand(new File(args[i]))); } if (sources.isEmpty()) { System.err.println("Nie znaleziono nic do zaindeksowania."); return; } System.out.printf("Indeksuję %d kopii (core %s)%n%n", sources.size(), CORE_VERSION); try (Database db = Database.open(dataDir)) { Ingestor ingestor = new Ingestor(db, CORE_VERSION); int ok = 0; int failed = 0; for (File source : sources) { long started = System.currentTimeMillis(); System.out.printf(" %-46s ", truncate(source.getName(), 46)); System.out.flush(); try { Ingestor.Result result = ingestor.ingest(source); long seconds = (System.currentTimeMillis() - started) / 1000; if ("unreadable".equals(result.status())) { System.out.printf("NIECZYTELNY: %s%n", result.statusNote()); failed++; continue; } System.out.printf("%5d plików %2ds%n", result.fileCount(), seconds); if (result.roots().isEmpty()) { System.out.printf(" └─ %s%n", "installer".equals(result.status()) ? "płyta instalacyjna — silnik nierozpoznawalny bez rozpakowania" : "silnik nierozpoznany"); } for (Ingestor.RootSummary root : result.roots()) { System.out.printf(" └─ %-12s %-13s %s%n", root.rootPath().isEmpty() ? "(korzeń)" : root.rootPath() + "/", root.engine(), root.edition() == null ? "" : truncate(root.edition(), 44)); } ok++; } catch (Exception e) { System.out.printf("BŁĄD: %s%n", e.getMessage()); failed++; } } System.out.printf("%nGotowe: %d zaindeksowanych, %d nieudanych.%n", ok, failed); } } private static void list(Path dataDir) throws Exception { try (Database db = Database.open(dataDir); Statement st = db.connection().createStatement(); ResultSet rs = st.executeQuery(""" SELECT c.display_name, c.container, c.status, c.file_count, g.root_path, COALESCE(g.engine_override, g.engine_detected) AS engine, MAX(CASE WHEN d.field = 'edition' THEN d.value END) AS edition, MAX(CASE WHEN d.field = 'family' THEN d.value END) AS family FROM copy c LEFT JOIN game_root g ON g.copy_id = c.id LEFT JOIN detection d ON d.copy_id = c.id AND d.root_path = COALESCE(g.root_path, '') GROUP BY c.id, g.id ORDER BY family, edition, c.display_name, g.root_path """)) { System.out.printf("%-40s %-10s %-10s %-13s %-22s %s%n", "KOPIA", "STATUS", "KORZEŃ", "SILNIK", "RODZINA", "WYDANIE"); while (rs.next()) { String root = rs.getString("root_path"); System.out.printf("%-40s %-10s %-10s %-13s %-22s %s%n", truncate(rs.getString("display_name"), 40), rs.getString("status"), root == null ? "—" : (root.isEmpty() ? "/" : root + "/"), nvl(rs.getString("engine")), nvl(rs.getString("family")), nvl(rs.getString("edition"))); } } } private static void stats(Path dataDir) throws Exception { try (Database db = Database.open(dataDir); Statement st = db.connection().createStatement()) { query(st, "kopie", "SELECT COUNT(*) FROM copy"); query(st, "pliki (wystąpienia)", "SELECT COUNT(*) FROM file"); query(st, "bloby (unikalne)", "SELECT COUNT(*) FROM blob"); query(st, "bloby współdzielone przez >1 kopię", "SELECT COUNT(*) FROM (SELECT blob_sha1 FROM file GROUP BY blob_sha1 " + "HAVING COUNT(DISTINCT copy_id) > 1)"); query(st, "zaszyfrowane skrypty", "SELECT COUNT(*) FROM blob WHERE encrypted = 1"); System.out.println("\nFormaty:"); try (ResultSet rs = st.executeQuery( "SELECT format, COUNT(*) n, SUM(size) bytes FROM blob " + "GROUP BY format ORDER BY n DESC LIMIT 15")) { while (rs.next()) { System.out.printf(" %-8s %6d %8.1f MB%n", nvl(rs.getString("format")), rs.getInt("n"), rs.getLong("bytes") / 1024.0 / 1024.0); } } } } private static void query(Statement st, String label, String sql) throws Exception { try (ResultSet rs = st.executeQuery(sql)) { System.out.printf("%-38s %d%n", label + ":", rs.getLong(1)); } } private static String nvl(String value) { return value == null ? "—" : value; } private static String truncate(String value, int max) { if (value == null) { return "—"; } return value.length() <= max ? value : value.substring(0, max - 1) + "…"; } private static void usage() { System.out.println(""" rex-catalog — katalog obrazów gier Aidem Media ingest <ścieżka...> zaindeksuj obraz, katalog gry lub katalog kolekcji list wypisz zaindeksowane kopie stats statystyki bazy Baza: -Dcatalog.data= (domyślnie ./data) """); } }