diff --git a/docs/instructions.md b/docs/instructions.md index ca136cd..17d4cd9 100644 --- a/docs/instructions.md +++ b/docs/instructions.md @@ -119,10 +119,32 @@ For each task completed: ## TODO List: -All tasks completed! The NFO database query issue has been resolved. +All tasks completed! Recent issues have been resolved. ## Recently Fixed Issues: +### ✅ Fixed: NFO Creation 500 Error - Missing Folder (2026-01-18) + +**Issue:** POST http://127.0.0.1:8000/api/nfo/blue-period/create returns 500 (Internal Server Error) with message "Series folder not found: /home/lukas/Volume/serien/Blue Period" + +**Root Cause:** The NFO service was checking if the series folder existed before creating NFO files, and raising a FileNotFoundError if it didn't. This prevented users from creating NFO files for newly added series where no episodes had been downloaded yet. + +**Solution:** Modified `create_tvshow_nfo()` in `nfo_service.py` to automatically create the series folder (with `mkdir(parents=True, exist_ok=True)`) if it doesn't exist, instead of raising an error. + +**Files Modified:** +- [src/core/services/nfo_service.py](../src/core/services/nfo_service.py) + +**Tests Added:** +- [tests/unit/test_nfo_service_folder_creation.py](../tests/unit/test_nfo_service_folder_creation.py) - Unit test verifying folder creation +- [tests/integration/test_nfo_folder_creation.py](../tests/integration/test_nfo_folder_creation.py) - Integration test for end-to-end verification + +**Verification:** +- Unit and integration tests pass +- NFO files can now be created for series even when the folder doesn't exist +- Folder is automatically created when needed + +--- + ### ✅ Fixed: NFO Service 503 Error (2026-01-18) **Issue:** Failed to load resource: the server responded with a status of 503 (Service Unavailable) when creating NFO files. @@ -132,6 +154,7 @@ All tasks completed! The NFO database query issue has been resolved. **Solution:** Updated `fastapi_app.py` startup code to sync NFO configuration (including TMDB API key) from `data/config.json` to `settings` object, similar to how `anime_directory` was already being synced. **Files Modified:** + - [src/server/fastapi_app.py](../src/server/fastapi_app.py) **Verification:** NFO endpoints now return 200 OK instead of 503, and NFO creation is functional. diff --git a/src/core/services/nfo_service.py b/src/core/services/nfo_service.py index 0d9a57e..53a6433 100644 --- a/src/core/services/nfo_service.py +++ b/src/core/services/nfo_service.py @@ -100,7 +100,8 @@ class NFOService: folder_path = self.anime_directory / serie_folder if not folder_path.exists(): - raise FileNotFoundError(f"Series folder not found: {folder_path}") + logger.info(f"Creating series folder: {folder_path}") + folder_path.mkdir(parents=True, exist_ok=True) async with self.tmdb_client: # Search for TV show diff --git a/tests/integration/test_nfo_folder_creation.py b/tests/integration/test_nfo_folder_creation.py new file mode 100644 index 0000000..5b97d01 --- /dev/null +++ b/tests/integration/test_nfo_folder_creation.py @@ -0,0 +1,114 @@ +"""Integration test for NFO creation with missing folder. + +Tests that NFO creation works when the series folder doesn't exist yet. +""" +from pathlib import Path +from unittest.mock import AsyncMock, patch + +import pytest + +from src.core.services.nfo_service import NFOService +from src.core.services.tmdb_client import TMDBClient + + +@pytest.mark.asyncio +async def test_nfo_creation_with_missing_folder_integration(): + """Integration test: NFO creation creates folder if missing.""" + # Use actual temp directory for this test + import tempfile + with tempfile.TemporaryDirectory() as tmpdir: + anime_dir = Path(tmpdir) + serie_folder = "Test Anime Series" + folder_path = anime_dir / serie_folder + + # Verify folder doesn't exist + assert not folder_path.exists() + + # Create NFO service + nfo_service = NFOService( + tmdb_api_key="test_key", + anime_directory=str(anime_dir), + image_size="original" + ) + + # Mock TMDB responses + mock_search = { + "results": [{ + "id": 99999, + "name": "Test Anime Series", + "first_air_date": "2023-01-01", + "overview": "Test", + "vote_average": 8.0 + }] + } + + mock_details = { + "id": 99999, + "name": "Test Anime Series", + "first_air_date": "2023-01-01", + "overview": "Test description", + "vote_average": 8.0, + "genres": [], + "networks": [], + "status": "Returning Series", + "number_of_seasons": 1, + "number_of_episodes": 10, + "poster_path": None, + "backdrop_path": None + } + + mock_ratings = {"results": []} + + # Patch TMDB client methods + with patch.object( + nfo_service.tmdb_client, 'search_tv_show', + new_callable=AsyncMock + ) as mock_search_method, \ + patch.object( + nfo_service.tmdb_client, 'get_tv_show_details', + new_callable=AsyncMock + ) as mock_details_method, \ + patch.object( + nfo_service.tmdb_client, 'get_tv_show_content_ratings', + new_callable=AsyncMock + ) as mock_ratings_method, \ + patch.object( + nfo_service, '_download_media_files', + new_callable=AsyncMock + ) as mock_download: + + mock_search_method.return_value = mock_search + mock_details_method.return_value = mock_details + mock_ratings_method.return_value = mock_ratings + mock_download.return_value = { + "poster": False, + "logo": False, + "fanart": False + } + + # Create NFO - this should create the folder + nfo_path = await nfo_service.create_tvshow_nfo( + serie_name="Test Anime Series", + serie_folder=serie_folder, + year=2023, + download_poster=False, + download_logo=False, + download_fanart=False + ) + + # Verify folder was created + assert folder_path.exists(), "Series folder should have been created" + assert folder_path.is_dir(), "Series folder should be a directory" + + # Verify NFO file exists + assert nfo_path.exists(), "NFO file should exist" + assert nfo_path.name == "tvshow.nfo", "NFO file should be named tvshow.nfo" + assert nfo_path.parent == folder_path, "NFO should be in series folder" + + # Verify NFO file has content + nfo_content = nfo_path.read_text() + assert "" in nfo_content, "NFO should contain tvshow tag" + assert "Test Anime Series" in nfo_content, "NFO should contain title" + + print(f"✓ Test passed: Folder created at {folder_path}") + print(f"✓ NFO file created at {nfo_path}") diff --git a/tests/unit/test_nfo_service_folder_creation.py b/tests/unit/test_nfo_service_folder_creation.py new file mode 100644 index 0000000..76af7b6 --- /dev/null +++ b/tests/unit/test_nfo_service_folder_creation.py @@ -0,0 +1,121 @@ +"""Unit tests for NFO service folder creation. + +Tests that the NFO service correctly creates series folders when they don't exist. +""" +import tempfile +from pathlib import Path +from unittest.mock import AsyncMock, patch + +import pytest + +from src.core.services.nfo_service import NFOService + + +class TestNFOServiceFolderCreation: + """Test NFO service creates folders when needed.""" + + @pytest.fixture + def temp_anime_dir(self): + """Create temporary anime directory.""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def nfo_service(self, temp_anime_dir): + """Create NFO service with temporary directory.""" + return NFOService( + tmdb_api_key="test_api_key", + anime_directory=str(temp_anime_dir), + image_size="original", + auto_create=False + ) + + @pytest.mark.asyncio + async def test_create_nfo_creates_missing_folder( + self, nfo_service, temp_anime_dir + ): + """Test that create_tvshow_nfo creates folder if it doesn't exist.""" + serie_folder = "Test Series" + folder_path = temp_anime_dir / serie_folder + + # Verify folder doesn't exist initially + assert not folder_path.exists() + + # Mock TMDB client responses + mock_search_results = { + "results": [ + { + "id": 12345, + "name": "Test Series", + "first_air_date": "2023-01-01", + "overview": "Test overview", + "vote_average": 8.5 + } + ] + } + + mock_details = { + "id": 12345, + "name": "Test Series", + "first_air_date": "2023-01-01", + "overview": "Test overview", + "vote_average": 8.5, + "genres": [{"id": 16, "name": "Animation"}], + "networks": [{"name": "Test Network"}], + "status": "Returning Series", + "number_of_seasons": 1, + "number_of_episodes": 12, + "poster_path": "/test_poster.jpg", + "backdrop_path": "/test_backdrop.jpg" + } + + mock_content_ratings = { + "results": [ + {"iso_3166_1": "DE", "rating": "12"} + ] + } + + with patch.object( + nfo_service.tmdb_client, 'search_tv_show', + new_callable=AsyncMock + ) as mock_search, \ + patch.object( + nfo_service.tmdb_client, 'get_tv_show_details', + new_callable=AsyncMock + ) as mock_details_call, \ + patch.object( + nfo_service.tmdb_client, 'get_tv_show_content_ratings', + new_callable=AsyncMock + ) as mock_ratings, \ + patch.object( + nfo_service, '_download_media_files', + new_callable=AsyncMock + ) as mock_download: + + mock_search.return_value = mock_search_results + mock_details_call.return_value = mock_details + mock_ratings.return_value = mock_content_ratings + mock_download.return_value = { + "poster": False, + "logo": False, + "fanart": False + } + + # Call create_tvshow_nfo + nfo_path = await nfo_service.create_tvshow_nfo( + serie_name="Test Series", + serie_folder=serie_folder, + year=2023, + download_poster=False, + download_logo=False, + download_fanart=False + ) + + # Verify folder was created + assert folder_path.exists() + assert folder_path.is_dir() + + # Verify NFO file was created + assert nfo_path.exists() + assert nfo_path.name == "tvshow.nfo" + assert nfo_path.parent == folder_path