feat: Implement NFOService.update_tvshow_nfo()

- Parse existing NFO to extract TMDB ID from uniqueid or tmdbid element
- Fetch fresh metadata from TMDB API
- Regenerate NFO with updated data
- Optionally re-download media files
- Add comprehensive error handling (missing NFO, no TMDB ID, invalid XML)
- Add unit tests for XML parsing logic (4 tests, all passing)
- Add integration test script (requires TMDB API key)
This commit is contained in:
2026-01-11 21:10:44 +01:00
parent 67119d0627
commit e32098fb94
4 changed files with 428 additions and 10 deletions

View File

@@ -0,0 +1,178 @@
"""Unit test for NFOService.update_tvshow_nfo() - tests XML parsing logic."""
import asyncio
from pathlib import Path
import tempfile
import shutil
from lxml import etree
import pytest
from src.core.services.nfo_service import NFOService
from src.core.services.tmdb_client import TMDBAPIError
def create_sample_nfo(tmdb_id: int = 1429) -> str:
"""Create a sample NFO XML with TMDB ID."""
return f'''<?xml version="1.0" encoding="UTF-8"?>
<tvshow>
<title>Attack on Titan</title>
<originaltitle>Shingeki no Kyojin</originaltitle>
<year>2013</year>
<plot>Several hundred years ago, humans were nearly exterminated by Titans.</plot>
<uniqueid type="tmdb" default="false">{tmdb_id}</uniqueid>
<uniqueid type="tvdb" default="true">267440</uniqueid>
<tmdbid>{tmdb_id}</tmdbid>
<tvdbid>267440</tvdbid>
</tvshow>'''
def test_parse_nfo_with_uniqueid():
"""Test parsing NFO with uniqueid elements."""
# Create temporary directory structure
temp_dir = Path(tempfile.mkdtemp())
serie_folder = temp_dir / "test_series"
serie_folder.mkdir()
nfo_path = serie_folder / "tvshow.nfo"
try:
# Write sample NFO
nfo_path.write_text(create_sample_nfo(1429), encoding="utf-8")
# Parse it (same logic as in update_tvshow_nfo)
tree = etree.parse(str(nfo_path))
root = tree.getroot()
# Extract TMDB ID
tmdb_id = None
for uniqueid in root.findall(".//uniqueid"):
if uniqueid.get("type") == "tmdb":
tmdb_id = int(uniqueid.text)
break
assert tmdb_id == 1429, f"Expected TMDB ID 1429, got {tmdb_id}"
print(f"✓ Successfully parsed TMDB ID from uniqueid: {tmdb_id}")
finally:
shutil.rmtree(temp_dir)
def test_parse_nfo_with_tmdbid_element():
"""Test parsing NFO with tmdbid element (fallback)."""
# Create NFO without uniqueid but with tmdbid element
nfo_content = '''<?xml version="1.0" encoding="UTF-8"?>
<tvshow>
<title>Test Show</title>
<tmdbid>12345</tmdbid>
</tvshow>'''
temp_dir = Path(tempfile.mkdtemp())
serie_folder = temp_dir / "test_series"
serie_folder.mkdir()
nfo_path = serie_folder / "tvshow.nfo"
try:
nfo_path.write_text(nfo_content, encoding="utf-8")
# Parse it
tree = etree.parse(str(nfo_path))
root = tree.getroot()
# Try uniqueid first (should fail)
tmdb_id = None
for uniqueid in root.findall(".//uniqueid"):
if uniqueid.get("type") == "tmdb":
tmdb_id = int(uniqueid.text)
break
# Fallback to tmdbid element
if tmdb_id is None:
tmdbid_elem = root.find(".//tmdbid")
if tmdbid_elem is not None and tmdbid_elem.text:
tmdb_id = int(tmdbid_elem.text)
assert tmdb_id == 12345, f"Expected TMDB ID 12345, got {tmdb_id}"
print(f"✓ Successfully parsed TMDB ID from tmdbid element: {tmdb_id}")
finally:
shutil.rmtree(temp_dir)
def test_parse_nfo_without_tmdb_id():
"""Test parsing NFO without TMDB ID raises appropriate error."""
# Create NFO without any TMDB ID
nfo_content = '''<?xml version="1.0" encoding="UTF-8"?>
<tvshow>
<title>Test Show</title>
</tvshow>'''
temp_dir = Path(tempfile.mkdtemp())
serie_folder = temp_dir / "test_series"
serie_folder.mkdir()
nfo_path = serie_folder / "tvshow.nfo"
try:
nfo_path.write_text(nfo_content, encoding="utf-8")
# Parse it
tree = etree.parse(str(nfo_path))
root = tree.getroot()
# Try to extract TMDB ID
tmdb_id = None
for uniqueid in root.findall(".//uniqueid"):
if uniqueid.get("type") == "tmdb":
tmdb_id = int(uniqueid.text)
break
if tmdb_id is None:
tmdbid_elem = root.find(".//tmdbid")
if tmdbid_elem is not None and tmdbid_elem.text:
tmdb_id = int(tmdbid_elem.text)
assert tmdb_id is None, "Should not have found TMDB ID"
print("✓ Correctly identified NFO without TMDB ID")
finally:
shutil.rmtree(temp_dir)
def test_parse_invalid_xml():
"""Test parsing invalid XML raises appropriate error."""
nfo_content = '''<?xml version="1.0" encoding="UTF-8"?>
<tvshow>
<title>Unclosed tag
</tvshow>'''
temp_dir = Path(tempfile.mkdtemp())
serie_folder = temp_dir / "test_series"
serie_folder.mkdir()
nfo_path = serie_folder / "tvshow.nfo"
try:
nfo_path.write_text(nfo_content, encoding="utf-8")
# Try to parse - should raise XMLSyntaxError
try:
tree = etree.parse(str(nfo_path))
assert False, "Should have raised XMLSyntaxError"
except etree.XMLSyntaxError:
print("✓ Correctly raised XMLSyntaxError for invalid XML")
finally:
shutil.rmtree(temp_dir)
if __name__ == "__main__":
print("Testing NFO XML parsing logic...")
print()
test_parse_nfo_with_uniqueid()
test_parse_nfo_with_tmdbid_element()
test_parse_nfo_without_tmdb_id()
test_parse_invalid_xml()
print()
print("=" * 60)
print("✓ ALL TESTS PASSED")
print("=" * 60)