Fix NFO service year extraction from series names
This commit is contained in:
@@ -120,85 +120,65 @@ For each task completed:
|
||||
## TODO List:
|
||||
|
||||
fix:
|
||||
Failed to load NFO/images for the-dreaming-boy-is-a-realist: No results found for: The Dreaming Boy is a Realist (2023)
|
||||
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
|
||||
│ /home/lukas/Volume/repo/Aniworld/src/server/services/background_loader_service.py:399 in │
|
||||
│ \_load_nfo_and_images │
|
||||
│ │
|
||||
│ 396 │ │ │ │
|
||||
│ 397 │ │ │ # Use existing NFOService to create NFO with all images │
|
||||
│ 398 │ │ │ # This reuses all existing TMDB API logic and image downloading │
|
||||
│ ❱ 399 │ │ │ nfo_path = await self.series_app.nfo_service.create_tvshow_nfo( │
|
||||
│ 400 │ │ │ │ serie_name=task.name, │
|
||||
│ 401 │ │ │ │ serie_folder=task.folder, │
|
||||
│ 402 │ │ │ │ year=task.year, │
|
||||
│ │
|
||||
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
|
||||
│ │ db = <sqlalchemy.ext.asyncio.session.AsyncSession object at 0x736aa1e0b770> │ │
|
||||
│ │ e = TMDBAPIError('No results found for: The Dreaming Boy is a Realist (2023)') │ │
|
||||
│ │ self = <src.server.services.background_loader_service.BackgroundLoaderService object at │ │
|
||||
│ │ 0x736aa27556a0> │ │
|
||||
│ │ task = SeriesLoadingTask( │ │
|
||||
│ │ │ key='the-dreaming-boy-is-a-realist', │ │
|
||||
│ │ │ folder='The Dreaming Boy is a Realist (2023)', │ │
|
||||
│ │ │ name='The Dreaming Boy is a Realist (2023)', │ │
|
||||
│ │ │ year=None, │ │
|
||||
│ │ │ status=<LoadingStatus.LOADING_NFO: 'loading_nfo'>, │ │
|
||||
│ │ │ progress={'episodes': True, 'nfo': False, 'logo': False, 'images': False}, │ │
|
||||
│ │ │ started_at=datetime.datetime(2026, 1, 19, 19, 37, 20, 540721, │ │
|
||||
│ │ tzinfo=datetime.timezone.utc), │ │
|
||||
│ │ │ completed_at=None, │ │
|
||||
│ │ │ error=None │ │
|
||||
│ │ ) │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ /home/lukas/Volume/repo/Aniworld/src/core/services/nfo_service.py:112 in create_tvshow_nfo │
|
||||
│ │
|
||||
│ 109 │ │ │ search_results = await self.tmdb_client.search_tv_show(serie_name) │
|
||||
│ 110 │ │ │ │
|
||||
│ 111 │ │ │ if not search_results.get("results"): │
|
||||
│ ❱ 112 │ │ │ │ raise TMDBAPIError(f"No results found for: {serie_name}") │
|
||||
│ 113 │ │ │ │
|
||||
│ 114 │ │ │ # Find best match (consider year if provided) │
|
||||
│ 115 │ │ │ tv_show = self.\_find_best_match(search_results["results"], serie_name, year) │
|
||||
│ │
|
||||
│ ╭─────────────────────────────────────────── locals ───────────────────────────────────────────╮ │
|
||||
│ │ download_fanart = True │ │
|
||||
│ │ download_logo = True │ │
|
||||
│ │ download_poster = True │ │
|
||||
│ │ folder_path = PosixPath('/mnt/server/serien/Serien/The Dreaming Boy is a Realist │ │
|
||||
│ │ (2023)') │ │
|
||||
│ │ search_results = {'page': 1, 'results': [], 'total_pages': 1, 'total_results': 0} │ │
|
||||
│ │ self = <src.core.services.nfo_service.NFOService object at 0x736aa273ce10> │ │
|
||||
│ │ serie_folder = 'The Dreaming Boy is a Realist (2023)' │ │
|
||||
│ │ serie_name = 'The Dreaming Boy is a Realist (2023)' │ │
|
||||
│ │ year = None │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
TMDBAPIError: No results found for: The Dreaming Boy is a Realist (2023)
|
||||
|
||||
INFO: 127.0.0.1:34916 - "POST /api/anime/search HTTP/1.1" 200
|
||||
INFO: 127.0.0.1:34916 - "POST /api/anime/add HTTP/1.1" 500
|
||||
ERROR: Exception in ASGI application
|
||||
Traceback (most recent call last):
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/anyio/streams/memory.py", line 98, in receive
|
||||
return self.receive_nowait()
|
||||
~~~~~~~~~~~~~~~~~~~^^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/anyio/streams/memory.py", line 93, in receive_nowait
|
||||
raise WouldBlock
|
||||
anyio.WouldBlock
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/base.py", line 78, in call_next
|
||||
message = await recv_stream.receive()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/anyio/streams/memory.py", line 118, in receive
|
||||
raise EndOfStream
|
||||
anyio.EndOfStream
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 426, in run_asgi
|
||||
result = await app( # type: ignore[func-returns-value]
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
self.scope, self.receive, self.send
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
)
|
||||
^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in **call**
|
||||
return await self.app(scope, receive, send)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/fastapi/applications.py", line 1106, in **call**
|
||||
await super().**call**(scope, receive, send)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/applications.py", line 122, in **call**
|
||||
await self.middleware_stack(scope, receive, send)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/errors.py", line 184, in **call**
|
||||
raise exc
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/errors.py", line 162, in **call**
|
||||
await self.app(scope, receive, \_send)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/base.py", line 108, in **call**
|
||||
response = await self.dispatch_func(request, call_next)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/lukas/Volume/repo/Aniworld/src/server/middleware/auth.py", line 209, in dispatch
|
||||
return await call_next(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/base.py", line 84, in call_next
|
||||
raise app_exc
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/base.py", line 70, in coro
|
||||
await self.app(scope, receive_or_disconnect, send_no_error)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/base.py", line 108, in **call**
|
||||
response = await self.dispatch_func(request, call_next)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/lukas/Volume/repo/Aniworld/src/server/middleware/setup_redirect.py", line 120, in dispatch
|
||||
return await call_next(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/base.py", line 84, in call_next
|
||||
raise app_exc
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/base.py", line 70, in coro
|
||||
await self.app(scope, receive_or_disconnect, send_no_error)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/cors.py", line 91, in **call**
|
||||
await self.simple_response(scope, receive, send, request_headers=headers)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/cors.py", line 146, in simple_response
|
||||
await self.app(scope, receive, send)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 79, in **call**
|
||||
raise exc
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 68, in **call**
|
||||
await self.app(scope, receive, sender)
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py", line 14, in **call**
|
||||
async with AsyncExitStack() as stack:
|
||||
~~~~~~~~~~~~~~^^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/contextlib.py", line 768, in **aexit**
|
||||
raise exc
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/contextlib.py", line 751, in **aexit**
|
||||
cb_suppress = await cb(\*exc_details)
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/lukas/miniconda3/envs/AniWorld/lib/python3.13/contextlib.py", line 271, in **aexit**
|
||||
raise RuntimeError("generator didn't stop after athrow()")
|
||||
RuntimeError: generator didn't stop after athrow()
|
||||
2026-01-19 20:37:37 [debug ] Message broadcast failed_count=0 message_type=series_loading_update recipient_count=0
|
||||
2026-01-19 20:37:37 [info ] Successfully loaded all data for series: the-dreaming-boy-is-a-realist
|
||||
2026-01-19 20:37:37 [info ] Processing loading task for series: bel-blatt
|
||||
2026-01-19 20:37:37 [debug ] Message broadcast failed_count=0 message_type=series_loading_update recipient_count=0
|
||||
INFO: Creating NFO for Übel Blatt (2025) (year: None)
|
||||
|
||||
@@ -9,8 +9,9 @@ Example:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from lxml import etree
|
||||
|
||||
@@ -58,6 +59,44 @@ class NFOService:
|
||||
self.image_size = image_size
|
||||
self.auto_create = auto_create
|
||||
|
||||
def has_nfo(self, serie_folder: str) -> bool:
|
||||
"""Check if tvshow.nfo exists for a series.
|
||||
|
||||
Args:
|
||||
serie_folder: Series folder name
|
||||
|
||||
Returns:
|
||||
True if NFO file exists
|
||||
"""
|
||||
nfo_path = self.anime_directory / serie_folder / "tvshow.nfo"
|
||||
return nfo_path.exists()
|
||||
|
||||
@staticmethod
|
||||
def _extract_year_from_name(serie_name: str) -> Tuple[str, Optional[int]]:
|
||||
"""Extract year from series name if present in format 'Name (YYYY)'.
|
||||
|
||||
Args:
|
||||
serie_name: Series name, possibly with year in parentheses
|
||||
|
||||
Returns:
|
||||
Tuple of (clean_name, year)
|
||||
- clean_name: Series name without year
|
||||
- year: Extracted year or None
|
||||
|
||||
Examples:
|
||||
>>> _extract_year_from_name("Attack on Titan (2013)")
|
||||
("Attack on Titan", 2013)
|
||||
>>> _extract_year_from_name("Attack on Titan")
|
||||
("Attack on Titan", None)
|
||||
"""
|
||||
# Match year in parentheses at the end: (YYYY)
|
||||
match = re.search(r'\((\d{4})\)\s*$', serie_name)
|
||||
if match:
|
||||
year = int(match.group(1))
|
||||
clean_name = serie_name[:match.start()].strip()
|
||||
return clean_name, year
|
||||
return serie_name, None
|
||||
|
||||
async def check_nfo_exists(self, serie_folder: str) -> bool:
|
||||
"""Check if tvshow.nfo exists for a series.
|
||||
|
||||
@@ -82,9 +121,10 @@ class NFOService:
|
||||
"""Create tvshow.nfo by scraping TMDB.
|
||||
|
||||
Args:
|
||||
serie_name: Name of the series to search
|
||||
serie_name: Name of the series to search (may include year in parentheses)
|
||||
serie_folder: Series folder name
|
||||
year: Release year (helps narrow search)
|
||||
year: Release year (helps narrow search). If None and name contains year,
|
||||
year will be auto-extracted
|
||||
download_poster: Whether to download poster.jpg
|
||||
download_logo: Whether to download logo.png
|
||||
download_fanart: Whether to download fanart.jpg
|
||||
@@ -96,7 +136,16 @@ class NFOService:
|
||||
TMDBAPIError: If TMDB API fails
|
||||
FileNotFoundError: If series folder doesn't exist
|
||||
"""
|
||||
logger.info(f"Creating NFO for {serie_name} (year: {year})")
|
||||
# Extract year from name if not provided
|
||||
clean_name, extracted_year = self._extract_year_from_name(serie_name)
|
||||
if year is None and extracted_year is not None:
|
||||
year = extracted_year
|
||||
logger.info(f"Extracted year {year} from series name")
|
||||
|
||||
# Use clean name for search
|
||||
search_name = clean_name
|
||||
|
||||
logger.info(f"Creating NFO for {search_name} (year: {year})")
|
||||
|
||||
folder_path = self.anime_directory / serie_folder
|
||||
if not folder_path.exists():
|
||||
@@ -104,15 +153,15 @@ class NFOService:
|
||||
folder_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async with self.tmdb_client:
|
||||
# Search for TV show
|
||||
logger.debug(f"Searching TMDB for: {serie_name}")
|
||||
search_results = await self.tmdb_client.search_tv_show(serie_name)
|
||||
# Search for TV show with clean name (without year)
|
||||
logger.debug(f"Searching TMDB for: {search_name}")
|
||||
search_results = await self.tmdb_client.search_tv_show(search_name)
|
||||
|
||||
if not search_results.get("results"):
|
||||
raise TMDBAPIError(f"No results found for: {serie_name}")
|
||||
raise TMDBAPIError(f"No results found for: {search_name}")
|
||||
|
||||
# Find best match (consider year if provided)
|
||||
tv_show = self._find_best_match(search_results["results"], serie_name, year)
|
||||
tv_show = self._find_best_match(search_results["results"], search_name, year)
|
||||
tv_id = tv_show["id"]
|
||||
|
||||
logger.info(f"Found match: {tv_show['name']} (ID: {tv_id})")
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user