### Match Printing Examples Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/specs/2026-05-05-verbose-mode-design.md Provides concrete examples of how Match objects are formatted for verbose output, demonstrating the inclusion of various fields like name, year, private status, and tags. ```text 2020:(11,15)+name=year 20:(11,13)+name=season+tags=[weak-duplicate,weak-episode] 2020:(11,15)+private+name=weak_duplicate+tags=[weak-duplicate,weak-episode] ``` -------------------------------- ### Year Extractor Implementation Example Source: https://github.com/asm0dey/guessit-java/blob/main/docs/architecture.md Demonstrates how to use PatternMatcher.regex to extract year candidates. Includes value mapping to Integer and validation for separators and a valid year range. ```java var opts = RegexOpts.defaults() .withValue(Integer::valueOf) .withValidator(m -> { if (!Validators.sepsSurround(input).test(m)) return false; int v = (Integer) m.value(); return 1920 <= v && v < 2030; }); for (var match : PatternMatcher.regex(input, PATTERN, "year", opts)) { ctx.matches.add(match); } ``` -------------------------------- ### Implement PatternMatcher Class Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Provides the initial structure for the `PatternMatcher` class, including package declaration and import statements for ArrayList and List. This is the starting point for implementing the matching logic. ```java package io.guessit.engine; import java.util.ArrayList; import java.util.List; ``` -------------------------------- ### Get User Configuration Paths Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Generates a list of potential user configuration file paths based on XDG_CONFIG_HOME environment variable or user's home directory. It checks for .json, .yml, and .yaml extensions in standard locations. ```java private static List userConfigPaths() { var paths = new ArrayList(); var xdg = System.getenv("XDG_CONFIG_HOME"); var home = System.getProperty("user.home"); Path xdgBase = xdg != null && !xdg.isBlank() ? Path.of(xdg) : Path.of(home, ".config"); for (var ext : List.of(".json", ".yml", ".yaml")) { paths.add(xdgBase.resolve("guessit").resolve("options" + ext)); } for (var ext : List.of(".json", ".yml", ".yaml")) { paths.add(Path.of(home, ".guessit", "options" + ext)); } return paths; } ``` -------------------------------- ### Get Group Start Index Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-2-phase2-source-language-country-releasegroup.md This is a helper method to retrieve the starting index of a named capture group from a Matcher object. It is used internally by the apply method to handle optional 'other' matches. ```java private static int groupStart(Matcher m, String name) { ``` -------------------------------- ### Copy Main Resources Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Creates directories and copies main configuration and data files from the Python guessit project. ```bash mkdir -p src/main/resources/io/guessit/config src/main/resources/io/guessit/data cp /tmp/guessit/guessit/config/options.json src/main/resources/io/guessit/config/options.json cp /tmp/guessit/guessit/data/tlds-alpha-by-domain.txt src/main/resources/io/guessit/data/tlds-alpha-by-domain.txt ``` -------------------------------- ### Guessit Entry Point Class Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Provides the main entry point for the Guessit library. It handles initialization with options, configuration loading, and running the parsing pipeline. Includes static methods for convenience parsing. ```java package io.guessit; import io.guessit.config.ConfigLoader; import io.guessit.config.OptionsConfig; import io.guessit.engine.ParseContext; import io.guessit.engine.Pipeline; import io.guessit.rules.Rules; public final class Guessit { private final Options options; private final OptionsConfig config; private final Pipeline pipeline; private Guessit(Options options) { this.options = options; this.config = ConfigLoader.load(options); this.pipeline = new Pipeline(Rules.defaultPipeline()); } public static Guessit withOptions(Options options) { return new Guessit(options); } public static GuessResult parse(String input) { return parse(input, Options.defaults()); } public static GuessResult parse(String input, Options options) { return withOptions(options).guess(input); } public GuessResult guess(String input) { var ctx = new ParseContext(input, options, config); pipeline.run(ctx); return ctx.result; } public java.util.Map> properties() { // Stub: no extractors yet, so no known properties. Returned for API completeness. return java.util.Map.of(); } public java.util.List suggestedExpected(java.util.Collection titles) { // Stub: returns unique titles unchanged. Real heuristic lands when title rule does (Plan 4). return titles.stream().distinct().toList(); } } ``` -------------------------------- ### Parameterized Parity Test Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/specs/2026-05-02-guessit-java-design.md Example of a parameterized test using JUnit 5 to verify parity with Python's test cases. ```APIDOC ### Parameterized parity test ```java @ParameterizedTest(name = "{0}") @MethodSource("allYmlCases") void ymlParity(YmlCase c) { var result = Guessit.parse(c.input(), c.options()).toMap(); if (c.negative()) { assertNotEquals(c.expected(), result); } else { assertEquals(c.expected(), result); } } static Stream allYmlCases() { return YmlTestLoader.discoverAll("yml/"); } ``` ``` -------------------------------- ### Build guessit-java with Maven Source: https://github.com/asm0dey/guessit-java/blob/main/README.md Use Maven to package the guessit-java project into JAR files for library and CLI usage. ```bash mvn package ``` -------------------------------- ### Parse Filename to YAML Output Source: https://context7.com/asm0dey/guessit-java/llms.txt Use the -y flag to get YAML output from the CLI. This is an alternative human-readable format for the extracted metadata. ```bash java -jar target/guessit-java-*-cli.jar -y \ "Movie.Name.2020.1080p.mkv" ``` -------------------------------- ### Parse Filename to JSON Output Source: https://context7.com/asm0dey/guessit-java/llms.txt Use the -j flag to get JSON output from the CLI. This is useful for programmatic consumption of the extracted metadata. ```bash java -jar target/guessit-java-*-cli.jar -j \ "Movie.Name.2020.1080p.mkv" ``` -------------------------------- ### Initial Project Commit with Git Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Stage and commit the initial project files. This includes configuration files like pom.xml and .gitignore, along with the README. ```bash git add .gitignore .gitattributes README.md pom.xml git commit -m "chore: maven project skeleton with java 25 + picocli/jackson/snakeyaml/commons-csv/junit5" ``` -------------------------------- ### Smoke Test CLI with Sample File Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-1-phase1-extractors.md Run the guessit-java CLI tool with a sample movie file to verify extractor functionality. The output shows parsed metadata like year, screen size, video codec, audio codec, and container. ```bash java -jar target/guessit-java-*-cli.jar "Movie.Name.2015.1080p.BluRay.x264.AAC-RG.mkv" ``` ```text year: 2015 screen_size: 1080p video_codec: H.264 audio_codec: AAC container: mkv ``` -------------------------------- ### Implement next() method in MatchSet Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-03-plan-4-phase4-title-episode-title.md Adds the next method to MatchSet, which finds the succeeding match after a given match, respecting a predicate and ordered by start position. ```java public Optional next(Match m, Predicate p) { return matches.stream() .filter(o -> o.start() >= m.end()) .filter(p) .min(Comparator.comparingInt(Match::start)); } ``` -------------------------------- ### Initialize Git Repository Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Initializes a new Git repository with the main branch. Navigate to your project directory before running this command. ```bash cd /home/finkel/work_self/guessit-java git init -b main ``` -------------------------------- ### Implement range() method in MatchSet Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-03-plan-4-phase4-title-episode-title.md Adds the range method to MatchSet, which filters matches to include only those within the specified start and end bounds and satisfying a predicate. ```java public Stream range(int start, int end, Predicate p) { return matches.stream() .filter(m -> m.start() >= start && m.end() <= end) .filter(p); } ``` -------------------------------- ### Build Options from Accumulator in Java Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Demonstrates how to construct an `Options` object using an `OptionsAccum` instance. This is useful for accumulating configuration settings before building the final options. ```java Boolean dateYearFirst, dateDayFirst, episodePreferNumber, enforceListWhenSingle; boolean noUserConfig, noDefaultConfig; final java.util.List expectedTitle = new java.util.ArrayList<>(); final java.util.List expectedGroup = new java.util.ArrayList<>(); final java.util.List excludes = new java.util.ArrayList<>(); final java.util.List includes = new java.util.ArrayList<>(); final java.util.List allowedLanguages = new java.util.ArrayList<>(); final java.util.List allowedCountries = new java.util.ArrayList<>(); final java.util.List configPaths = new java.util.ArrayList<>(); final java.util.Map raw = new java.util.LinkedHashMap<>(); Options build() { return Options.builder() .type(type).name(name) .expectedTitle(expectedTitle).expectedGroup(expectedGroup) .excludes(excludes).includes(includes) .allowedLanguages(allowedLanguages).allowedCountries(allowedCountries) .dateYearFirst(dateYearFirst).dateDayFirst(dateDayFirst) .episodePreferNumber(episodePreferNumber).enforceListWhenSingle(enforceListWhenSingle) .configPaths(configPaths) .noUserConfig(noUserConfig).noDefaultConfig(noDefaultConfig) .raw(raw) .build(); } } ``` -------------------------------- ### Guessit.withOptions(Options) + Guessit#guess(String) Source: https://context7.com/asm0dey/guessit-java/llms.txt Creates a reusable Guessit instance that retains the compiled pipeline and loaded configuration across multiple calls. This is preferred when parsing large batches of filenames to avoid repeated configuration loading. ```APIDOC ## Guessit.withOptions(Options) + Guessit#guess(String) — reusable instance Creates a `Guessit` instance that retains the compiled pipeline and loaded config across multiple calls. Preferred when parsing large batches of filenames — avoids repeated config loading. ```java import io.guessit.Guessit; import io.guessit.OptionsBuilder; var guessit = Guessit.withOptions(OptionsBuilder.options().type("episode").build()); String[] filenames = { "Breaking.Bad.S01E01.720p.BluRay.mkv", "Breaking.Bad.S01E02.1080p.WEB-DL.mkv", "Breaking.Bad.S01E03.HDTV.x264.mkv" }; for (var fn : filenames) { var r = guessit.guess(fn); System.out.printf("S%02dE%02d %-10s %s%n", r.season(), r.episode(), r.screenSize(), r.source()); } // S01E01 720p Blu-ray // S01E02 1080p Web // S01E03 null HDTV ``` ``` -------------------------------- ### Define Marker Record in Java Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Represents a segment of text with a name, start and end positions, and raw value. Includes helper methods for checking containment and coverage. ```java package io.guessit.engine; public record Marker(String name, int start, int end, String raw) { public boolean contains(int pos) { return pos >= start && pos < end; } public boolean covers(int s, int e) { return s >= start && e <= end; } public int length() { return end - start; } } ``` -------------------------------- ### Test range() method in MatchSet Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-03-plan-4-phase4-title-episode-title.md Verifies that the range method returns matches that are fully contained within the specified start and end boundaries and satisfy a given predicate. ```java @Test void rangeReturnsMatchesFullyInsideSpan() { var set = new MatchSet(); set.add(Match.of("a", 1, 0, 5, "00000")); set.add(Match.of("b", 2, 6, 10, "1111")); set.add(Match.of("c", 3, 11, 15, "2222")); var inRange = set.range(0, 10, m -> true).toList(); assertEquals(2, inRange.size()); assertEquals("a", inRange.get(0).name()); assertEquals("b", inRange.get(1).name()); } ``` -------------------------------- ### Run guessit-java CLI Source: https://github.com/asm0dey/guessit-java/blob/main/README.md Execute the guessit-java command-line interface to parse a video filename. The JAR file is located in the target directory. ```bash java -jar target/guessit-java--cli.jar "Movie.Name.2020.1080p.mkv" ``` -------------------------------- ### Implement upgradeUltraHdBluray logic Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-2-phase2-source-language-country-releasegroup.md Upgrades 'Blu-ray' source matches to 'Ultra HD Blu-ray' if a '2160p' screen size is found within the same filepart. ```java private void upgradeUltraHdBluray(ParseContext ctx) { var bds = ctx.matches.named("source") .filter(m -> "Blu-ray".equals(m.value())) .toList(); if (bds.isEmpty()) return; for (var filepart : ctx.markers) { if (!"path".equals(filepart.name())) continue; for (var bd : bds) { if (!filepart.covers(bd.start(), bd.end())) continue; boolean has2160p = ctx.matches.named("screen_size") .anyMatch(m -> "2160p".equals(m.value()) && filepart.covers(m.start(), m.end())); if (!has2160p) continue; ctx.matches.replace(bd, new Match("source", "Ultra HD Blu-ray", bd.start(), bd.end(), bd.raw(), bd.priority(), bd.tags(), bd.isPrivate())); } } } ``` -------------------------------- ### ExtractorPhase Emits Header and Per-Extractor Diff Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-05-verbose-mode.md This modification to ExtractorPhase adds tracing for the phase start and emits a trace diff for each extractor, showing changes in matches before and after extraction. ```java @Override public void apply(ParseContext ctx) { ctx.trace.phase("extractors"); for (var e : extractors) { var before = ctx.matches.snapshot(); ctx.trace.step("extract", e.name()); e.extract(ctx); TraceDiff.emit(before, ctx.matches.snapshot(), ctx.trace); } } ``` -------------------------------- ### Parse YML Content to Map Keys Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Parses content lines to map keys to their line numbers, ignoring lines that do not start with '?'. Ensures each key from the provided set is mapped only once. ```java var map = new java.util.HashMap(); var lines = content.split("\n", -1); var remaining = new java.util.HashSet<>(keys); for (int i = 0; i < lines.length; i++) { var trimmed = lines[i].trim(); if (!trimmed.startsWith("?")) continue; var literal = trimmed.substring(1).trim(); for (var k : new java.util.ArrayList<>(remaining)) { if (k.toString().equals(literal)) { map.put(k, i + 1); remaining.remove(k); break; } } } return map; } } ``` -------------------------------- ### Verify Build and Tests with Maven Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-1-phase1-extractors.md Execute a clean build and verify all unit tests pass. Ensure parity is at least 20%. ```bash mvn -q clean verify ``` -------------------------------- ### Smoke Test CLI with Input Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Run the shaded CLI jar with a sample input file to check its processing. With no rules defined in Plan 0, an empty JSON object is expected as output, and the command should exit successfully. ```bash java -jar target/guessit-java-0.1.0-SNAPSHOT-cli.jar --json "Test.mkv" ``` -------------------------------- ### Implement ConflictSolver Logic Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md The core implementation of the ConflictSolver class. It sorts matches by priority, length, and start time, then iterates to remove overlapping matches unless they have the 'coexist' tag. ```java package io.guessit.engine; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; public final class ConflictSolver { private ConflictSolver() {} public static void solve(MatchSet matches) { var all = matches.all().toList(); // Comparator: priority desc, length desc, start asc, name asc (stable tiebreak) var cmp = Comparator .comparingInt((Match m) -> m.priority()).reversed() .thenComparing(Comparator.comparingInt(Match::length).reversed()) .thenComparingInt(Match::start) .thenComparing(Match::name); var sorted = new ArrayList<>(all); sorted.sort(cmp); var kept = new ArrayList(); var dropped = new HashSet(); for (var candidate : sorted) { boolean dropThis = false; for (var winner : kept) { if (candidate.overlaps(winner) && !candidate.tags().contains("coexist") && !winner.tags().contains("coexist")) { dropThis = true; break; } } if (dropThis) dropped.add(candidate); else kept.add(candidate); } for (var d : dropped) matches.remove(d); } } ``` -------------------------------- ### Build and Use GuessIt Java CLI Source: https://context7.com/asm0dey/guessit-java/llms.txt Build the shaded CLI jar using 'mvn package'. Use the jar to parse filenames from the command line, mirroring the Python guessit CLI interface. ```bash # Build mvn package # Basic parse (plain key: value output) java -jar target/guessit-java-0.1.0-SNAPSHOT-cli.jar \ "The.Mandalorian.S03E08.2160p.WEB-DL.DDP5.1.Atmos.HDR.H265-GROUP.mkv" # title: The Mandalorian # season: 3 # episode: 8 ``` -------------------------------- ### Test previous() and next() methods in MatchSet Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-03-plan-4-phase4-title-episode-title.md Ensures that the previous and next methods correctly identify adjacent matches based on their start and end positions, while respecting a given predicate. ```java @Test void previousAndNextRespectPredicate() { var set = new MatchSet(); var a = Match.of("a", 1, 0, 3, "aaa"); var b = Match.of("b", 2, 5, 8, "bbb"); var c = Match.of("c", 3, 10, 13, "ccc"); set.add(a); set.add(b); set.add(c); assertEquals(a, set.previous(b, m -> true).orElseThrow()); assertEquals(c, set.next(b, m -> true).orElseThrow()); assertTrue(set.previous(a, m -> true).isEmpty()); } ``` -------------------------------- ### Smoke test CLI with sample input Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-2-phase2-source-language-country-releasegroup.md Test the command-line interface with a sample video file name to verify that the guessit-java parser correctly extracts relevant information like season, episode, title, source, and release group. ```bash java -jar target/guessit-java-*-cli.jar "Show.S01E02.Pilot.DVDRip.x264-CS.mkv" ``` -------------------------------- ### ExtractorPostPhase Emits Header and Per-Extractor Diff Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-05-verbose-mode.md This modification to ExtractorPostPhase adds tracing for the phase start and emits a trace diff for each extractor's post-processing step, highlighting changes in matches. ```java @Override public void apply(ParseContext ctx) { ctx.trace.phase("extractor_post"); for (var e : extractors) { var before = ctx.matches.snapshot(); ctx.trace.step("post", e.name()); e.postProcess(ctx); TraceDiff.emit(before, ctx.matches.snapshot(), ctx.trace); } } ``` -------------------------------- ### Guessit CLI Implementation Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md The main `GuessitCli` class implementing the command-line interface using picocli. It defines parameters for filenames and various options to control parsing behavior, including output formats and configuration. ```java package io.guessit.cli; import io.guessit.Guessit; import io.guessit.Options; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; @Command( name = "guessit-java", mixinStandardHelpOptions = true, versionProvider = GuessitCli.VersionProvider.class, description = "Parse video filenames into structured metadata." ) public final class GuessitCli implements Callable { @Parameters(arity = "0..*", description = "Filenames to parse.") List filenames = new ArrayList<>(); @Option(names = {"-t", "--type"}, description = "movie or episode hint.") String type; @Option(names = {"-n", "--name"}, description = "Override input name.") String name; @Option(names = {"-Y", "--date-year-first"}) boolean dateYearFirst; @Option(names = {"-D", "--date-day-first"}) boolean dateDayFirst; @Option(names = {"-L", "--allowed-language"}, arity = "1..*") List allowedLanguages = new ArrayList<>(); @Option(names = {"-C", "--allowed-country"}, arity = "1..*") List allowedCountries = new ArrayList<>(); @Option(names = {"-E", "--episode-prefer-number"}) boolean episodePreferNumber; @Option(names = {"-T", "--expected-title"}, arity = "1..*") List expectedTitles = new ArrayList<>(); @Option(names = {"-G", "--expected-group"}, arity = "1..*") List expectedGroups = new ArrayList<>(); @Option(names = "--excludes", arity = "1..*") List excludes = new ArrayList<>(); @Option(names = "--includes", arity = "1..*") List includes = new ArrayList<>(); @Option(names = {"-c", "--config"}, arity = "1..*") List configs = new ArrayList<>(); @Option(names = "--no-user-config") boolean noUserConfig; @Option(names = "--no-default-config") boolean noDefaultConfig; @Option(names = {"-j", "--json"}) boolean json; @Option(names = {"-y", "--yaml"}) boolean yaml; @Option(names = {"-v", "--verbose"}) boolean verbose; @Option(names = {"-P", "--show-property"}) String showProperty; @Option(names = "--advanced") boolean advanced; @Override public Integer call() { if (filenames.isEmpty()) { System.err.println("No input filename provided. See --help."); return 2; } var opts = Options.builder() .type(type) .name(name) .expectedTitle(expectedTitles) .expectedGroup(expectedGroups) .excludes(excludes) .includes(includes) ``` -------------------------------- ### ConflictPhase Emits Header and Whole-Phase Diff Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-05-verbose-mode.md This update to ConflictPhase adds tracing for the phase start and emits a single trace diff summarizing all changes to matches after the conflict resolution process. ```java @Override public void apply(ParseContext ctx) { ctx.trace.phase("conflicts"); var before = ctx.matches.snapshot(); ConflictSolver.solve(ctx.matches); TraceDiff.emit(before, ctx.matches.snapshot(), ctx.trace); } ``` -------------------------------- ### MarkerPhase Emits Header and Per-Marker Note Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-05-verbose-mode.md This update to MarkerPhase adds tracing for the phase start and emits a note for each new marker produced, including its raw value, start/end positions, and name. ```java @Override public void apply(ParseContext ctx) { ctx.trace.phase("markers"); var beforeAll = List.copyOf(ctx.markers); for (var p : producers) { p.produce(ctx); } for (var m : ctx.markers) { if (!beforeAll.contains(m)) { ctx.trace.note("marker: " + m.raw() + ":(" + m.start() + "," + m.end() ")+"name=" + m.name()); } } } ``` ```java import java.util.List; ``` -------------------------------- ### Test ConfigLoader Loads Bundled Defaults Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Verifies that `ConfigLoader.load()` correctly loads bundled default configurations when `Options.defaults()` is provided and `noDefaultConfig` is not set. ```java package io.guessit.config; import io.guessit.Options; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.*; class ConfigLoaderTest { @Test void loadsBundledDefaults() { var cfg = ConfigLoader.load(Options.defaults()); // bundled options.json has top-level "advanced_config" key per Python guessit assertNotNull(cfg); assertFalse(cfg.raw().isEmpty(), "bundled config must not be empty"); } @Test void noDefaultConfigSkipsBundle() { var opts = Options.builder().noDefaultConfig(true).noUserConfig(true).build(); var cfg = ConfigLoader.load(opts); assertTrue(cfg.raw().isEmpty()); } @Test void explicitConfigOverridesScalars(@TempDir Path tmp) throws IOException { var f = tmp.resolve("override.json"); Files.writeString(f, "{\"some_key\": \"override\"}"); var opts = Options.builder() .noDefaultConfig(true) .noUserConfig(true) .configPaths(java.util.List.of(f)) .build(); var cfg = ConfigLoader.load(opts); assertEquals("override", cfg.raw().get("some_key")); } @Test void mergeListsConcatenates(@TempDir Path tmp) throws IOException { var a = tmp.resolve("a.json"); var b = tmp.resolve("b.json"); Files.writeString(a, "{\"items\": [1, 2]}"); Files.writeString(b, "{\"items\": [3]}"); var opts = Options.builder() .noDefaultConfig(true) .noUserConfig(true) .configPaths(java.util.List.of(a, b)) .build(); var cfg = ConfigLoader.load(opts); assertEquals(java.util.List.of(1, 2, 3), cfg.raw().get("items")); } @Test void mergeMapsDeep(@TempDir Path tmp) throws IOException { var a = tmp.resolve("a.json"); var b = tmp.resolve("b.json"); Files.writeString(a, "{\"nested\": {\"x\": 1, \"y\": 2}}"); Files.writeString(b, "{\"nested\": {\"y\": 3, \"z\": 4}}"); var opts = Options.builder() .noDefaultConfig(true).noUserConfig(true) .configPaths(java.util.List.of(a, b)).build(); var cfg = ConfigLoader.load(opts); @SuppressWarnings("unchecked") var nested = (java.util.Map) cfg.raw().get("nested"); assertEquals(1, nested.get("x")); assertEquals(3, nested.get("y")); assertEquals(4, nested.get("z")); } } ``` -------------------------------- ### ConflictSolver Test Cases Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Contains JUnit tests for the ConflictSolver class, covering scenarios like higher priority wins, longer matches on tie priority, earlier start wins, coexistence tags, and no overlap. ```java package io.guessit.engine; import org.junit.jupiter.api.Test; import java.util.List; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; class ConflictSolverTest { @Test void higherPriorityWinsOverlap() { var s = new MatchSet(); s.add(Match.of("year", 2020, 0, 4, "2020").withPriority(2000)); s.add(Match.of("season", 20, 0, 2, "20").withPriority(1000)); ConflictSolver.solve(s); var names = s.all().map(Match::name).collect(Collectors.toList()); assertEquals(List.of("year"), names); } @Test void longerWinsOnTiePriority() { var s = new MatchSet(); s.add(Match.of("a", 1, 0, 4, "abcd")); s.add(Match.of("b", 2, 0, 2, "ab")); ConflictSolver.solve(s); assertEquals(List.of("a"), s.all().map(Match::name).collect(Collectors.toList())); } @Test void earlierStartWinsOnTiePriorityAndLength() { var s = new MatchSet(); s.add(Match.of("a", 1, 2, 4, "ab")); s.add(Match.of("b", 2, 0, 2, "cd")); ConflictSolver.solve(s); // both length 2, same priority, b starts earlier — both keep since they don't overlap assertEquals(2, s.all().count()); } @Test void coexistTagSurvives() { var s = new MatchSet(); s.add(Match.of("country", "FR", 0, 2, "FR").withPriority(1000)); s.add(Match.of("language", "fr", 0, 2, "fr").withPriority(1000).withTags(java.util.Set.of("coexist"))); ConflictSolver.solve(s); assertEquals(2, s.all().count()); } @Test void noOverlapKeepsAll() { var s = new MatchSet(); s.add(Match.of("a", 1, 0, 2, "ab")); s.add(Match.of("b", 2, 5, 7, "cd")); ConflictSolver.solve(s); assertEquals(2, s.all().count()); } } ``` -------------------------------- ### Execute Guessit Main Class with Maven Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Executes the Guessit main class using Maven's exec plugin. This is a smoke test to ensure the entry point functions correctly, redirecting stderr to null and ignoring exit codes. ```bash mvn -q exec:java -Dexec.mainClass=io.guessit.Guessit -Dexec.args="" 2>/dev/null || true ``` -------------------------------- ### Custom Configuration Source: https://context7.com/asm0dey/guessit-java/llms.txt Provide a path to a custom configuration file (JSON or YAML) using the -c flag. This configuration will be deep-merged with the default settings. ```bash java -jar target/guessit-java-*-cli.jar \ -c /path/to/my-options.json \ "My.Show.S01E01.mkv" ``` -------------------------------- ### Drop Private Affixes in Language Extractor Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-2-phase2-source-language-country-releasegroup.md This method removes private affix matches related to language and subtitle language from the parse context. It filters matches ending with '.prefix' or '.suffix' and starting with 'language' or 'subtitle_language'. ```java private void dropPrivateAffixes(ParseContext ctx) { // Affix matches were emitted as private to drive renaming; remove them now. var toRemove = ctx.matches.all() .filter(m -> m.name().endsWith(".prefix") || m.name().endsWith(".suffix")) .filter(m -> m.name().startsWith("language") || m.name().startsWith("subtitle_language")) .toList(); for (var m : toRemove) ctx.matches.remove(m); } ``` -------------------------------- ### Run Maven Package Command Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-04-plan-5-phase5-longtail.md Execute the Maven package command to build the project, skipping tests. This command is used to create the project's JAR files. ```bash mvn -q -DskipTests package ``` -------------------------------- ### Parse Filename with Verbose Tracing Source: https://context7.com/asm0dey/guessit-java/llms.txt Use the `guess` method with a `Trace` sink to get a phase-by-phase breakdown of the parsing process. This is useful for debugging extraction issues. Ensure `PrintTrace` is used to write the trace to an `Appendable` like `System.out`. ```java import io.guessit.Guessit; import io.guessit.Options; import io.guessit.engine.PrintTrace; var guessit = Guessit.withOptions(Options.defaults()); var trace = new PrintTrace(System.out); guessit.guess("Interstellar.2014.2160p.UHD.BluRay.DTS-HD.MA.7.1.x265-GROUP.mkv", trace); ``` -------------------------------- ### Test EnlargeGroupMatches Processor Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-04-plan-5-phase5-longtail.md This test verifies that the EnlargeGroupMatches processor correctly expands a match to include surrounding brackets. It sets up a ParseContext with a marker and a match, applies the processor, and asserts that the match's start and end boundaries are adjusted to encompass the brackets. ```java package io.guessit.rules.post; import io.guessit.engine.*; import org.junit.jupiter.api.Test; import java.util.*; import static org.assertj.core.api.Assertions.assertThat; class EnlargeGroupMatchesTest { @Test void enlargeBracketedMatchToIncludeBrackets() { var ctx = new ParseContext("[XCT]Show", null, null); ctx.markers.add(new Marker("group", 0, 5)); // includes [ and ] ctx.matches.add(new Match("release_group", "XCT", 1, 4, "XCT", 1000, Set.of(), false)); new EnlargeGroupMatches().apply(ctx); var m = ctx.matches.named("release_group").findFirst().orElseThrow(); assertThat(m.start()).isEqualTo(0); assertThat(m.end()).isEqualTo(5); } } ``` -------------------------------- ### Pipeline Execution Flow Source: https://github.com/asm0dey/guessit-java/blob/main/docs/architecture.md Illustrates the sequence of phases executed by the Pipeline.run(ctx) method, from initial parsing to final output assembly. ```text input string │ ▼ ┌──────────────────────────────────────────────────────┐ │ ParseContext (input, options, config, markers, MatchSet, result) │ └──────────────────────────────────────────────────────┘ │ ▼ Pipeline.run(ctx) — phases applied in fixed order: │ 1. MarkerPhase split path + bracket/paren groups 2. ExtractorPhase each Extractor.extract() adds Matches 3. ConflictPhase resolve overlapping Matches 4. ExtractorPostPhase each Extractor.postProcess() refines/renames 5. PostPhase cross-cutting cleanup (paths, private, title marker) 6. OutputPhase assemble GuessResult into ctx.result ``` -------------------------------- ### AbsoluteEpisodeRule: postProcess Implementation Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-04-plan-5-phase5-longtail.md Implements the `postProcess` method for `AbsoluteEpisodeRule`. It groups episode matches by their enclosing markers or start position, coalescing 'range-filled' successors. It then processes groups to identify and assign absolute episode numbers based on Python's `RenameToAbsoluteEpisode` logic. ```java @Override public void postProcess(ParseContext ctx) { var pathMarkers = ctx.markers.stream() .filter(m -> m.name().equals("path")) .toList(); for (var fp : pathMarkers) { var eps = ctx.matches.snapshot().stream() .filter(m -> m.name().equals("episode")) .filter(m -> m.start() >= fp.start() && m.end() <= fp.end()) .sorted(java.util.Comparator.comparingInt(io.guessit.engine.Match::start)) .toList(); if (eps.isEmpty()) continue; // Group eps by enclosing group marker (or "no-group" bucket keyed by start position run). var groups = new java.util.LinkedHashMap>(); for (var e : eps) { var enc = ctx.markers.stream() .filter(g -> g.name().equals("group") && g.start() <= e.start() && g.end() >= e.end()) .findFirst().orElse(null); var key = enc != null ? enc : "noenc-" + e.start(); // Coalesce range-filled successors into the same key as the previous match. if (e.tags().contains("range-filled")) { var lastKey = groups.keySet().stream().reduce((a,b)->b).orElse(key); key = lastKey; } groups.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(e); } var multi = groups.values().stream().filter(g -> g.size() >= 2).toList(); if (multi.size() == 2) { var sorted = multi.stream() .sorted(java.util.Comparator.comparingInt(g -> g.getLast().end())) .toList(); ``` -------------------------------- ### Run Tests Command Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Executes Maven tests for ConfigLoaderTest and PipelineTest classes. Expects both classes to pass with 5 and 1 tests respectively, and 0 failures. ```bash mvn -q -Dtest='ConfigLoaderTest,PipelineTest' test ``` -------------------------------- ### Implement Separator Validators Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-1-phase1-extractors.md This Java code provides implementations for separator-aware validation predicates. `sepsBefore` checks if the character before the match start is a separator or if the match is at the beginning of the string. `sepsAfter` checks if the character after the match end is a separator or if the match is at the end of the string. `sepsSurround` combines both. ```java package io.guessit.engine; import java.util.function.Predicate; public final class Validators { private Validators() {} public static Predicate sepsBefore(String input) { return m -> m.start() == 0 || Seps.isSep(input.charAt(m.start() - 1)); } public static Predicate sepsAfter(String input) { return m -> m.end() == input.length() || Seps.isSep(input.charAt(m.end())); } public static Predicate sepsSurround(String input) { var before = sepsBefore(input); var after = sepsAfter(input); return m -> before.test(m) && after.test(m); } } ``` -------------------------------- ### Implement EnlargeGroupMatches PostProcessor Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-04-plan-5-phase5-longtail.md This Java class implements the PostProcessor interface to enlarge group matches. It iterates through markers, and for each 'group' marker, it checks existing matches. If a match starts one character after the group marker or ends one character before it, its boundaries are adjusted to include the marker's boundaries. This ensures that bracketed content is fully captured. ```java package io.guessit.rules.post; import io.guessit.engine.*; import java.util.ArrayList; import java.util.List; public final class EnlargeGroupMatches implements PostProcessor { @Override public void apply(ParseContext ctx) { for (var g : ctx.markers) { if (!g.name().equals("group")) continue; var snapshot = new ArrayList<>(ctx.matches.snapshot()); for (var m : snapshot) { Match next = null; if (m.start() == g.start() + 1) { next = m.withStart(g.start()); } else if (m.end() == g.end() - 1) { next = m.withEnd(g.end()); } if (next != null) ctx.matches.replace(m, next); } } } } ``` -------------------------------- ### Reusable Guessit Instance for Batch Parsing Source: https://context7.com/asm0dey/guessit-java/llms.txt Create a reusable Guessit instance with specific options for parsing multiple filenames efficiently. This avoids repeated configuration loading. ```java import io.guessit.Guessit; import io.guessit.OptionsBuilder; var guessit = Guessit.withOptions(OptionsBuilder.options().type("episode").build()); String[] filenames = { "Breaking.Bad.S01E01.720p.BluRay.mkv", "Breaking.Bad.S01E02.1080p.WEB-DL.mkv", "Breaking.Bad.S01E03.HDTV.x264.mkv" }; for (var fn : filenames) { var r = guessit.guess(fn); System.out.printf("S%02dE%02d %-10s %s%n", r.season(), r.episode(), r.screenSize(), r.source()); } // S01E01 720p Blu-ray // S01E02 1080p Web // S01E03 null HDTV ``` -------------------------------- ### Run Tests with Maven Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-1-phase1-extractors.md Executes all tests within the `io.guessit.engine` package using Maven. This command is used to verify the successful integration of new features and ensure existing functionality remains intact. ```bash mvn -q -pl . test -Dtest='io.guessit.engine.*' ``` -------------------------------- ### Guessit.parse(String input) Source: https://context7.com/asm0dey/guessit-java/llms.txt The simplest entry point for parsing a filename with all default settings. It returns a fully typed GuessResult object. ```APIDOC ## Guessit.parse(String input) — zero-config static parse The simplest entry point. Parses a filename with all default settings and returns a fully typed `GuessResult`. ```java import io.guessit.Guessit; import io.guessit.GuessResult; GuessResult r = Guessit.parse("The.Dark.Knight.2008.1080p.BluRay.x264-GROUP.mkv"); System.out.println(r.title()); // The Dark Knight System.out.println(r.year()); // 2008 System.out.println(r.screenSize()); // 1080p System.out.println(r.source()); // Blu-ray System.out.println(r.videoCodec()); // [H.264] System.out.println(r.container()); // mkv System.out.println(r.releaseGroup()); // GROUP System.out.println(r.type()); // movie ``` ``` -------------------------------- ### Commit Java Files with Git Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Stages and commits the newly created Marker.java and Match.java files to the Git repository with a descriptive message. ```bash git add src/main/java/io/guessit/engine/Marker.java src/main/java/io/guessit/engine/Match.java git commit -m "engine: marker and match records" ``` -------------------------------- ### Guessit Entry Point Overload with Trace Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/specs/2026-05-05-verbose-mode-design.md Introduces a new public overload for the Guessit.guess method that accepts a Trace object. This method initializes the ParseContext with the provided trace and delegates to the pipeline execution. The existing guess(String) method is updated to delegate to this new overload with Trace.NOOP. ```java public GuessResult guess(String input, Trace trace) { var ctx = new ParseContext(input, options, config); ctx.trace = trace; trace.input(input); pipeline.run(ctx); trace.result(ctx.result); return ctx.result; } ``` -------------------------------- ### Stage and Commit Java Files Source: https://github.com/asm0dey/guessit-java/blob/main/docs/superpowers/plans/2026-05-02-plan-0-foundation.md Stages the newly created `Rules.java` and `Guessit.java` files and commits them to the Git repository with a descriptive message. ```bash git add src/main/java/io/guessit/rules/Rules.java src/main/java/io/guessit/Guessit.java git commit -m "api: Rules registry stub + Guessit entry point" ```