Fix NFO service year extraction from series names

This commit is contained in:
2026-01-19 20:42:04 +01:00
parent 6d40ddbfe5
commit 01f828c799
3 changed files with 287 additions and 90 deletions

View File

@@ -146,6 +146,70 @@ class TestFSKRatingExtraction:
assert fsk is None
class TestYearExtraction:
"""Test year extraction from series names."""
def test_extract_year_with_year(self, nfo_service):
"""Test extraction when year is present in format (YYYY)."""
clean_name, year = nfo_service._extract_year_from_name("Attack on Titan (2013)")
assert clean_name == "Attack on Titan"
assert year == 2013
def test_extract_year_without_year(self, nfo_service):
"""Test extraction when no year is present."""
clean_name, year = nfo_service._extract_year_from_name("Attack on Titan")
assert clean_name == "Attack on Titan"
assert year is None
def test_extract_year_multiple_parentheses(self, nfo_service):
"""Test extraction with multiple parentheses - only last one with year."""
clean_name, year = nfo_service._extract_year_from_name("Series (Part 1) (2023)")
assert clean_name == "Series (Part 1)"
assert year == 2023
def test_extract_year_with_trailing_spaces(self, nfo_service):
"""Test extraction with trailing spaces."""
clean_name, year = nfo_service._extract_year_from_name("Attack on Titan (2013) ")
assert clean_name == "Attack on Titan"
assert year == 2013
def test_extract_year_parentheses_not_year(self, nfo_service):
"""Test extraction when parentheses don't contain a year."""
clean_name, year = nfo_service._extract_year_from_name("Series (Special Edition)")
assert clean_name == "Series (Special Edition)"
assert year is None
def test_extract_year_invalid_year_format(self, nfo_service):
"""Test extraction with invalid year format (not 4 digits)."""
clean_name, year = nfo_service._extract_year_from_name("Series (23)")
assert clean_name == "Series (23)"
assert year is None
def test_extract_year_future_year(self, nfo_service):
"""Test extraction with future year."""
clean_name, year = nfo_service._extract_year_from_name("Future Series (2050)")
assert clean_name == "Future Series"
assert year == 2050
def test_extract_year_old_year(self, nfo_service):
"""Test extraction with old year."""
clean_name, year = nfo_service._extract_year_from_name("Classic Series (1990)")
assert clean_name == "Classic Series"
assert year == 1990
def test_extract_year_real_world_example(self, nfo_service):
"""Test extraction with the real-world example from the bug report."""
clean_name, year = nfo_service._extract_year_from_name("The Dreaming Boy is a Realist (2023)")
assert clean_name == "The Dreaming Boy is a Realist"
assert year == 2023
def test_extract_year_uebel_blatt(self, nfo_service):
"""Test extraction with Übel Blatt example."""
clean_name, year = nfo_service._extract_year_from_name("Übel Blatt (2025)")
assert clean_name == "Übel Blatt"
assert year == 2025
class TestTMDBToNFOModel:
"""Test conversion of TMDB data to NFO model."""
@@ -221,6 +285,110 @@ class TestTMDBToNFOModel:
class TestCreateTVShowNFO:
"""Test NFO creation workflow."""
@pytest.mark.asyncio
async def test_create_nfo_with_year_in_name(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de):
"""Test NFO creation when year is included in series name.
This test addresses the bug where searching TMDB with year in the name
(e.g., "The Dreaming Boy is a Realist (2023)") fails to find results.
"""
# Setup
serie_name = "The Dreaming Boy is a Realist (2023)"
serie_folder = "The Dreaming Boy is a Realist (2023)"
(tmp_path / serie_folder).mkdir()
# Mock TMDB responses
search_results = {"results": [mock_tmdb_data]}
with patch.object(nfo_service.tmdb_client, '__aenter__', return_value=nfo_service.tmdb_client):
with patch.object(nfo_service.tmdb_client, '__aexit__', return_value=None):
with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search:
with patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details:
with patch.object(nfo_service.tmdb_client, 'get_tv_show_content_ratings', new_callable=AsyncMock) as mock_ratings:
with patch.object(nfo_service.image_downloader, 'download_poster', new_callable=AsyncMock):
with patch.object(nfo_service.image_downloader, 'download_logo', new_callable=AsyncMock):
with patch.object(nfo_service.image_downloader, 'download_fanart', new_callable=AsyncMock):
mock_search.return_value = search_results
mock_details.return_value = mock_tmdb_data
mock_ratings.return_value = mock_content_ratings_de
# Act
nfo_path = await nfo_service.create_tvshow_nfo(
serie_name=serie_name,
serie_folder=serie_folder,
year=None # Year should be auto-extracted
)
# Assert - should search with clean name "The Dreaming Boy is a Realist"
mock_search.assert_called_once_with("The Dreaming Boy is a Realist")
# Verify NFO file was created
assert nfo_path.exists()
assert nfo_path.name == "tvshow.nfo"
@pytest.mark.asyncio
async def test_create_nfo_year_parameter_takes_precedence(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de):
"""Test that explicit year parameter takes precedence over extracted year."""
# Setup
serie_name = "Attack on Titan (2013)"
serie_folder = "Attack on Titan"
explicit_year = 2015 # Different from extracted year
(tmp_path / serie_folder).mkdir()
# Mock TMDB responses
search_results = {"results": [mock_tmdb_data]}
with patch.object(nfo_service.tmdb_client, '__aenter__', return_value=nfo_service.tmdb_client):
with patch.object(nfo_service.tmdb_client, '__aexit__', return_value=None):
with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search:
with patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details:
with patch.object(nfo_service.tmdb_client, 'get_tv_show_content_ratings', new_callable=AsyncMock) as mock_ratings:
with patch.object(nfo_service.image_downloader, 'download_poster', new_callable=AsyncMock):
with patch.object(nfo_service.image_downloader, 'download_logo', new_callable=AsyncMock):
with patch.object(nfo_service.image_downloader, 'download_fanart', new_callable=AsyncMock):
with patch.object(nfo_service, '_find_best_match') as mock_find_match:
mock_search.return_value = search_results
mock_details.return_value = mock_tmdb_data
mock_ratings.return_value = mock_content_ratings_de
mock_find_match.return_value = mock_tmdb_data
# Act
await nfo_service.create_tvshow_nfo(
serie_name=serie_name,
serie_folder=serie_folder,
year=explicit_year # Explicit year provided
)
# Assert - should use explicit year, not extracted year
mock_find_match.assert_called_once()
call_args = mock_find_match.call_args
assert call_args[0][2] == explicit_year # Third argument is year
@pytest.mark.asyncio
async def test_create_nfo_no_results_with_clean_name(self, nfo_service, tmp_path):
"""Test error handling when TMDB returns no results even with clean name."""
# Setup
serie_name = "Nonexistent Series (2023)"
serie_folder = "Nonexistent Series (2023)"
(tmp_path / serie_folder).mkdir()
with patch.object(nfo_service.tmdb_client, '__aenter__', return_value=nfo_service.tmdb_client):
with patch.object(nfo_service.tmdb_client, '__aexit__', return_value=None):
with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search:
mock_search.return_value = {"results": []}
# Act & Assert
with pytest.raises(TMDBAPIError) as exc_info:
await nfo_service.create_tvshow_nfo(
serie_name=serie_name,
serie_folder=serie_folder
)
# Should use clean name in error message
assert "No results found for: Nonexistent Series" in str(exc_info.value)
# Should have searched with clean name
mock_search.assert_called_once_with("Nonexistent Series")
@pytest.mark.asyncio
async def test_create_nfo_with_fsk(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de):
"""Test NFO creation includes FSK rating."""