diff --git a/VERSION b/VERSION index 7ee7020..59e9e60 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.10 +1.0.11 diff --git a/engine/episode_match.py b/engine/episode_match.py index 476f740..43f7aa3 100644 --- a/engine/episode_match.py +++ b/engine/episode_match.py @@ -269,21 +269,46 @@ def _combined_title_variants(name: str) -> list[str]: return variants -def _combined_match_score(fnorm: str, combined_name: str) -> float: +def _combined_match_score(fnorm: str, combined_name: str, file_title: str = "") -> float: variants = _combined_title_variants(combined_name) - return max((_similarity(fnorm, v) for v in variants), default=0.0) + best = max((_similarity(fnorm, v) for v in variants), default=0.0) + if file_title and _dual_title_matches_combined(file_title, combined_name): + best = max(best, 0.96) + return best -def _combined_allowed_for_file(parsed: dict, target: EpisodeTarget) -> bool: - """Only treat as multi-episode when the file's episode code fits the range start.""" +def _dual_title_matches_combined(file_title: str, combined_name: str) -> bool: + """True when the filename lists both stories in a combined-order pair.""" + file_parts = [p.strip() for p in re.split(r"\s+-\s+", file_title) if p.strip()] + combined_parts = split_combined_title(combined_name) + if len(file_parts) < 2 or len(combined_parts) < 2: + return False + matched = 0 + for fp in file_parts[: len(combined_parts)]: + fn = normalize_title(fp) + if max(_similarity(fn, normalize_title(cp)) for cp in combined_parts) >= 0.72: + matched += 1 + return matched >= 2 + + +def _combined_allowed_for_file( + parsed: dict, + target: EpisodeTarget, + combined_name: str = "", +) -> bool: + """Treat as multi-episode when the episode tag or dual title fits the aired range.""" if target.span <= 1: return True if parsed.get("span", 1) > 1: return True + file_title = parsed.get("title") or "" + if combined_name and _dual_title_matches_combined(file_title, combined_name): + return True file_ep = parsed.get("old_first") if file_ep is None: return False - return file_ep == target.episode + end = target.episode_end if target.episode_end is not None else target.episode + return file_ep == target.episode or file_ep == end def _range_overlaps( @@ -390,9 +415,9 @@ def match_filenames_to_episodes( for _cep, target, variants in combined_entries: if not fnorm: continue - if not _combined_allowed_for_file(parsed, target): + if not _combined_allowed_for_file(parsed, target, _cep.name): continue - best = _combined_match_score(fnorm, _cep.name) + best = _combined_match_score(fnorm, _cep.name, raw_title) best = _apply_season_hint(best, target, parsed) pairs.append((best, fname, target, raw_title, _cep.name)) diff --git a/packaging/release-stamp-1.0.11.txt b/packaging/release-stamp-1.0.11.txt new file mode 100644 index 0000000..4cec97c --- /dev/null +++ b/packaging/release-stamp-1.0.11.txt @@ -0,0 +1,3 @@ +HSRename 1.0.11 +- Fix multi-episode detection when filename uses end episode (S01E06 for E05-E06) +- Detect dual titles separated by " - " (Otto 3000 - Night Prowler) as combined pairs