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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user