fix(folder-rename): prevent duplicate year suffixes in series folder names

Use regex to strip all trailing year suffixes before adding the canonical
one, preventing duplication like 'Show (2021) (2021) (2021)'.

- Add regex pattern (\s*\(\d{4}\))+\s*$ to remove all existing year suffixes
- Ensure idempotent behavior across multiple folder rename runs
- Add 7 unit tests covering the bug cases and edge scenarios

Fixes: 86 Eighty Six (2021) (2021)..., Alma-chan (2025) (2025)...
This commit is contained in:
2026-05-19 21:24:07 +02:00
parent 7bcd0600d5
commit 75c22fe296
2 changed files with 90 additions and 1 deletions

View File

@@ -66,6 +66,9 @@ def _parse_nfo_title_and_year(nfo_path: Path) -> Tuple[Optional[str], Optional[s
def _compute_expected_folder_name(title: str, year: str) -> str:
"""Compute the expected folder name from title and year.
Removes any existing year suffixes (e.g., "(2021)") before adding the
canonical one to prevent duplication across multiple folder rename runs.
Args:
title: Series title from NFO.
year: Release year from NFO.
@@ -73,7 +76,15 @@ def _compute_expected_folder_name(title: str, year: str) -> str:
Returns:
Sanitised folder name in the format ``"{title} ({year})"``.
"""
raw_name = f"{title} ({year})"
import re
# Remove all trailing year suffixes to prevent duplication.
# This handles cases where the title already contains one or more years.
# Regex pattern: matches one or more " (YYYY)" at the end of the string
clean_title = re.sub(r'(\s*\(\d{4}\))+\s*$', '', title).strip()
year_suffix = f" ({year})"
raw_name = f"{clean_title}{year_suffix}"
return sanitize_folder_name(raw_name)

View File

@@ -75,6 +75,84 @@ class TestComputeExpectedFolderName:
result = _compute_expected_folder_name("A / B", "2021")
assert result == "A B (2021)"
def test_does_not_duplicate_year(self) -> None:
result = _compute_expected_folder_name("86 Eighty Six (2021)", "2021")
assert result == "86 Eighty Six (2021)"
assert result.count("(2021)") == 1
def test_removes_duplicate_year_suffixes_bug_86_eighty_six(self) -> None:
"""Test the bug fix for duplicate year suffixes.
Issue: "86 Eighty Six (2021) (2021) (2021) (2021) (2021)"
should become "86 Eighty Six (2021)"
"""
result = _compute_expected_folder_name(
"86 Eighty Six (2021) (2021) (2021) (2021) (2021)", "2021"
)
assert result == "86 Eighty Six (2021)"
assert result.count("(2021)") == 1
def test_removes_duplicate_year_suffixes_alma_chan(self) -> None:
"""Test the bug fix for duplicate year suffixes with long title.
Issue: "Alma-chan Wants to Be a Family! (2025) (2025) (2025) (2025) (2025)"
should become "Alma-chan Wants to Be a Family! (2025)"
"""
result = _compute_expected_folder_name(
"Alma-chan Wants to Be a Family! (2025) (2025) (2025) (2025) (2025)",
"2025",
)
assert result == "Alma-chan Wants to Be a Family! (2025)"
assert result.count("(2025)") == 1
def test_removes_duplicate_year_suffixes_bogus_skill(self) -> None:
"""Test the bug fix for duplicate year suffixes with very long title.
Issue: Long title with duplicated years should be cleaned.
"""
result = _compute_expected_folder_name(
"Bogus Skill Fruitmaster About That Time I Became Able to Eat "
"Unlimited Numbers of Skill Fruits (That Kill You) (2025) (2025)",
"2025",
)
assert "(2025)" in result
assert result.count("(2025)") == 1
def test_removes_multiple_different_year_suffixes(self) -> None:
"""Test that old duplicate years are removed and new one added."""
result = _compute_expected_folder_name(
"Series (2020) (2020) (2020)", "2021"
)
assert result == "Series (2021)"
assert "(2020)" not in result
assert result.count("(2021)") == 1
def test_handles_whitespace_with_duplicate_years(self) -> None:
"""Test that extra whitespace is removed along with duplicate years."""
result = _compute_expected_folder_name(
"Series (2021) (2021) (2021) ", "2021"
)
assert result == "Series (2021)"
assert result.count("(2021)") == 1
assert not result.endswith(" ")
def test_idempotent_multiple_calls(self) -> None:
"""Test that calling the function multiple times produces the same result."""
title = "86 Eighty Six (2021) (2021) (2021)"
year = "2021"
# First call
result1 = _compute_expected_folder_name(title, year)
# Second call with the result
result2 = _compute_expected_folder_name(result1, year)
# Third call with the result
result3 = _compute_expected_folder_name(result2, year)
# All results should be identical
assert result1 == result2 == result3
assert result1 == "86 Eighty Six (2021)"
assert result1.count("(2021)") == 1
class TestIsSeriesBeingDownloaded:
"""Tests for _is_series_being_downloaded."""