fix async handling in SerieScanner and add image_downloader cleanup

- SerieScanner.scan() now detects running event loop and uses create_task()
  when already in async context, avoids RuntimeError
- NFOService.close() now also closes image_downloader to prevent resource leaks
- Add integration tests for TMDBClient lifecycle management

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-27 20:47:29 +02:00
parent b9c55f9e7a
commit d358a07290
3 changed files with 86 additions and 2 deletions

View File

@@ -44,8 +44,13 @@ class SerieScanner:
in keyDict and can be retrieved after scanning.
Example:
# Synchronous context (CLI):
scanner = SerieScanner("/path/to/anime", loader)
scanner.scan()
scanner.scan() # asyncio.run() used internally when no event loop
# Asynchronous context (server/scheduler):
# scan() detects running event loop and uses create_task()
# internally, so no special handling needed by caller.
# Results are in scanner.keyDict
# With DB lookup fallback:
@@ -432,7 +437,14 @@ class SerieScanner:
# Persist to database (async)
try:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
# No running loop — safe to use asyncio.run()
asyncio.run(self._persist_serie_to_db(serie))
else:
# Already in async context — schedule as task
asyncio.create_task(self._persist_serie_to_db(serie))
except Exception as e:
logger.warning(
"DB persistence failed for '%s', "

View File

@@ -784,6 +784,7 @@ class NFOService:
async def close(self):
"""Clean up resources."""
await self.tmdb_client.close()
await self.image_downloader.close()
async def create_minimal_nfo(
self,

View File

@@ -444,6 +444,77 @@ class TestTMDBClientSessionLeak:
"Unexpected warning about unclosed session"
class TestTMDBClientLifecycleIntegration:
"""Integration tests for TMDBClient lifecycle management."""
@pytest.mark.asyncio
async def test_context_manager_no_resource_warning(self, caplog):
"""Test async with TMDBClient produces no ResourceWarning."""
import logging
import warnings
caplog.set_level(logging.WARNING)
# Use context manager properly - should not leak
async with TMDBClient(api_key="test_key") as client:
await client._ensure_session()
assert client.session is not None
# Session should be closed after context exit
assert client.session is None or client.session.closed
@pytest.mark.asyncio
async def test_exception_safety_during_api_call(self, caplog):
"""Test session is closed even when exception raised during API call."""
import logging
caplog.set_level(logging.WARNING)
close_called = False
class TrackingSession:
def __init__(self):
self.closed = False
async def close(self):
nonlocal close_called
close_called = True
self.closed = True
async def get(self, url, **kwargs):
raise TMDBAPIError("Simulated API failure")
client = TMDBClient(api_key="test_key")
client.session = TrackingSession()
# Exception during context should still close session
with pytest.raises(TMDBAPIError):
async with client:
raise TMDBAPIError("Simulated API failure")
assert close_called, "Session was not closed after API exception"
@pytest.mark.asyncio
async def test_reuse_session_across_multiple_requests(self, caplog):
"""Test session is reused across multiple requests without leaks."""
import logging
caplog.set_level(logging.WARNING)
client = TMDBClient(api_key="test_key")
async with client as c:
# First request
await c._ensure_session()
session1 = c.session
# Second request should reuse same session
await c._ensure_session()
session2 = c.session
assert session1 is session2, "Session should be reused"
# After context exit, session should be closed
assert client.session is None or client.session.closed
class TestTMDBClientConnectorClosed:
"""Test handling of 'Connector is closed' errors."""