Compare commits
4 Commits
fc4e52f1a2
...
v1.1.18
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ded5a6e4d | |||
| d596902ca3 | |||
| d358a07290 | |||
| b9c55f9e7a |
@@ -1 +1 @@
|
||||
v1.1.17
|
||||
v1.1.18
|
||||
|
||||
@@ -181,6 +181,22 @@ scheduler = AsyncIOScheduler(jobstores=jobstores)
|
||||
|
||||
**If server is down >1 hour:** No automatic recovery. Manual trigger via `POST /api/scheduler/trigger-rescan` or wait for next scheduled run.
|
||||
|
||||
### Database Session Management
|
||||
|
||||
`get_async_session_factory()` returns a **new AsyncSession instance** directly (not a factory). The function name is historical — callers receive the session immediately:
|
||||
|
||||
```python
|
||||
# Correct usage:
|
||||
db = get_async_session_factory() # db IS the session
|
||||
await db.execute(...)
|
||||
await db.commit()
|
||||
await db.close()
|
||||
```
|
||||
|
||||
Do NOT call the result again with `()` — that tries to call an `AsyncSession` object, causing `'AsyncSession' object is not callable`.
|
||||
|
||||
For context manager usage, prefer `get_db_session()` (auto-commits) or `get_transactional_session()` (manual commit).
|
||||
|
||||
### Health Check Endpoints
|
||||
|
||||
The application provides health check endpoints for monitoring and container orchestration:
|
||||
|
||||
@@ -246,6 +246,7 @@ NFO files are created in the anime directory:
|
||||
<genre>Action</genre>
|
||||
<genre>Sci-Fi & Fantasy</genre>
|
||||
<uniqueid type="tmdb">1429</uniqueid>
|
||||
<tmdbid>1429</tmdbid>
|
||||
<thumb aspect="poster">https://image.tmdb.org/t/p/w500/...</thumb>
|
||||
<fanart>
|
||||
<thumb>https://image.tmdb.org/t/p/original/...</thumb>
|
||||
@@ -253,6 +254,13 @@ NFO files are created in the anime directory:
|
||||
</tvshow>
|
||||
```
|
||||
|
||||
**Manual TMDB ID Override**: To skip TMDB search and use a specific ID directly, include `<tmdbid>YOUR_ID</tmdbid>` in the NFO. This is useful when:
|
||||
- TMDB search fails for your series (e.g., new or obscure anime)
|
||||
- You already know the correct TMDB ID
|
||||
- You want to avoid rate limiting from repeated searches
|
||||
|
||||
Aniworld reads `<tmdbid>` element and `<uniqueid type="tmdb">` first. If found, it uses the ID directly instead of searching.
|
||||
|
||||
### 4.3 Episode NFO Format
|
||||
|
||||
```xml
|
||||
@@ -629,6 +637,36 @@ Every poster check action is logged:
|
||||
4. Check network speed to TMDB servers
|
||||
5. Verify disk I/O performance
|
||||
|
||||
### 6.7 TMDB Lookup Fails for My Series
|
||||
|
||||
**Problem**: TMDB search fails with "No results found" for a valid series.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check if series exists on TMDB**: Visit https://www.themoviedb.org and search for your series
|
||||
2. **Use manual ID override**: Add TMDB ID directly to `tvshow.nfo`:
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<tvshow>
|
||||
<title>Your Series Name</title>
|
||||
<tmdbid>12345</tmdbid>
|
||||
<uniqueid type="tmdb">12345</uniqueid>
|
||||
</tvshow>
|
||||
```
|
||||
Aniworld will use this ID directly instead of searching.
|
||||
|
||||
3. **Try alternative titles**: Some anime have different titles (Japanese, romaji, English). If you have access to the folder, rename it to match the TMDB title.
|
||||
|
||||
4. **Add to existing NFO**: If `tvshow.nfo` exists but has no TMDB ID, edit it to add:
|
||||
```xml
|
||||
<tmdbid>YOUR_TMDB_ID</tmdbid>
|
||||
```
|
||||
Then use the Update endpoint to refresh metadata.
|
||||
|
||||
5. **Check for rate limiting**: If many lookups fail at once, you may be hitting TMDB rate limits. Wait and retry later.
|
||||
|
||||
6. **Verify API key**: Ensure your TMDB API key is valid and has not exceeded usage limits.
|
||||
|
||||
---
|
||||
|
||||
## 7. Best Practices
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aniworld-web",
|
||||
"version": "1.1.17",
|
||||
"version": "1.1.18",
|
||||
"description": "Aniworld Anime Download Manager - Web Frontend",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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:
|
||||
@@ -218,8 +223,7 @@ class SerieScanner:
|
||||
try:
|
||||
from src.server.database.connection import get_async_session_factory
|
||||
|
||||
session_factory = get_async_session_factory()
|
||||
db = session_factory()
|
||||
db = get_async_session_factory()
|
||||
try:
|
||||
existing = await AnimeSeriesService.get_by_key(db, serie.key)
|
||||
if existing:
|
||||
@@ -433,7 +437,14 @@ class SerieScanner:
|
||||
|
||||
# Persist to database (async)
|
||||
try:
|
||||
asyncio.run(self._persist_serie_to_db(serie))
|
||||
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', "
|
||||
|
||||
@@ -163,58 +163,89 @@ class NFOService:
|
||||
logger.info("Creating series folder: %s", folder_path)
|
||||
folder_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Check for existing NFO with TMDB ID to skip search
|
||||
nfo_path = folder_path / "tvshow.nfo"
|
||||
existing_ids = None
|
||||
if nfo_path.exists():
|
||||
try:
|
||||
existing_ids = self.parse_nfo_ids(nfo_path)
|
||||
if existing_ids.get("tmdb_id"):
|
||||
logger.info(
|
||||
"Found existing TMDB ID %s in NFO, using directly",
|
||||
existing_ids["tmdb_id"]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("Could not parse existing NFO IDs: %s", e)
|
||||
|
||||
try:
|
||||
await self.tmdb_client._ensure_session()
|
||||
|
||||
# Search for TV show - try multiple strategies
|
||||
tv_show, search_source = await self._search_with_fallback(
|
||||
search_name, year, alt_titles
|
||||
)
|
||||
tv_id = tv_show["id"]
|
||||
# Use existing TMDB ID if found, otherwise search
|
||||
if existing_ids and existing_ids.get("tmdb_id"):
|
||||
tv_id = existing_ids["tmdb_id"]
|
||||
logger.info("Fetching details directly for TMDB ID: %s", tv_id)
|
||||
details = await self.tmdb_client.get_tv_show_details(
|
||||
tv_id,
|
||||
append_to_response="credits,external_ids,images"
|
||||
)
|
||||
content_ratings = await self.tmdb_client.get_tv_show_content_ratings(tv_id)
|
||||
tv_show = {"id": tv_id, "name": details.get("name", serie_name)}
|
||||
search_source = "nfo_override"
|
||||
else:
|
||||
# Search for TV show - try multiple strategies
|
||||
tv_show, search_source = await self._search_with_fallback(
|
||||
search_name, year, alt_titles
|
||||
)
|
||||
tv_id = tv_show["id"]
|
||||
|
||||
logger.info("Found match: %s (ID: %s)", tv_show['name'], tv_id)
|
||||
|
||||
# Get detailed information with multi-language image support
|
||||
details = await self.tmdb_client.get_tv_show_details(
|
||||
tv_id,
|
||||
append_to_response="credits,external_ids,images"
|
||||
)
|
||||
# Skip if we already fetched details via nfo_override
|
||||
if search_source != "nfo_override":
|
||||
details = await self.tmdb_client.get_tv_show_details(
|
||||
tv_id,
|
||||
append_to_response="credits,external_ids,images"
|
||||
)
|
||||
|
||||
# Get content ratings for FSK
|
||||
content_ratings = await self.tmdb_client.get_tv_show_content_ratings(tv_id)
|
||||
# Get content ratings for FSK
|
||||
content_ratings = await self.tmdb_client.get_tv_show_content_ratings(tv_id)
|
||||
|
||||
# Enrich with fallback languages for empty overview/tagline
|
||||
# Pass search result overview as last resort fallback
|
||||
search_overview = tv_show.get("overview") or None
|
||||
if not search_overview:
|
||||
try:
|
||||
logger.debug(
|
||||
"No overview in German search result, trying en-US search fallback for: %s",
|
||||
search_name,
|
||||
)
|
||||
en_search_results = await self.tmdb_client.search_tv_show(
|
||||
search_name,
|
||||
language="en-US",
|
||||
)
|
||||
if en_search_results.get("results"):
|
||||
en_match = self._find_best_match(
|
||||
en_search_results["results"], search_name, year
|
||||
# Enrich with fallback languages for empty overview/tagline
|
||||
# Pass search result overview as last resort fallback
|
||||
search_overview = tv_show.get("overview") or None
|
||||
if not search_overview:
|
||||
try:
|
||||
logger.debug(
|
||||
"No overview in German search result, trying en-US search fallback for: %s",
|
||||
search_name,
|
||||
)
|
||||
search_overview = en_match.get("overview") or None
|
||||
if search_overview:
|
||||
logger.info(
|
||||
"Using en-US search overview fallback for %s",
|
||||
search_name,
|
||||
en_search_results = await self.tmdb_client.search_tv_show(
|
||||
search_name,
|
||||
language="en-US",
|
||||
)
|
||||
if en_search_results.get("results"):
|
||||
en_match = self._find_best_match(
|
||||
en_search_results["results"], search_name, year
|
||||
)
|
||||
except (TMDBAPIError, Exception) as exc:
|
||||
logger.warning(
|
||||
"Failed en-US search fallback for overview: %s",
|
||||
exc,
|
||||
)
|
||||
search_overview = en_match.get("overview") or None
|
||||
if search_overview:
|
||||
logger.info(
|
||||
"Using en-US search overview fallback for %s",
|
||||
search_name,
|
||||
)
|
||||
except (TMDBAPIError, Exception) as exc:
|
||||
logger.warning(
|
||||
"Failed en-US search fallback for overview: %s",
|
||||
exc,
|
||||
)
|
||||
|
||||
details = await self._enrich_details_with_fallback(
|
||||
details, search_overview=search_overview
|
||||
)
|
||||
details = await self._enrich_details_with_fallback(
|
||||
details, search_overview=search_overview
|
||||
)
|
||||
else:
|
||||
# When using nfo_override, content_ratings already fetched
|
||||
pass
|
||||
|
||||
# Convert TMDB data to TVShowNFO model
|
||||
nfo_model = tmdb_to_nfo_model(
|
||||
@@ -646,21 +677,45 @@ class NFOService:
|
||||
{"query": normalized, "year": year, "lang": "de-DE", "desc": f"normalized:{normalized}"}
|
||||
)
|
||||
|
||||
# Strategy 6: Try search/multi for series indexed as movies
|
||||
search_strategies.append(
|
||||
{"query": primary_query, "year": year, "lang": "en-US", "desc": "multi_search", "use_multi": True}
|
||||
)
|
||||
|
||||
last_error = None
|
||||
for strategy in search_strategies:
|
||||
query = strategy["query"]
|
||||
lang = strategy["lang"]
|
||||
desc = strategy["desc"]
|
||||
use_multi = strategy.get("use_multi", False)
|
||||
|
||||
try:
|
||||
logger.debug(
|
||||
"TMDB search attempt: query='%s', lang=%s, year=%s, strategy=%s",
|
||||
query, lang, strategy["year"], desc
|
||||
)
|
||||
search_results = await self.tmdb_client.search_tv_show(
|
||||
query,
|
||||
language=lang
|
||||
)
|
||||
|
||||
# Use search/multi for multi_search strategy
|
||||
if use_multi:
|
||||
search_results = await self.tmdb_client.search_multi(
|
||||
query,
|
||||
language=lang
|
||||
)
|
||||
# Filter for TV shows only
|
||||
if search_results.get("results"):
|
||||
tv_results = [
|
||||
r for r in search_results["results"]
|
||||
if r.get("media_type") == "tv"
|
||||
]
|
||||
if tv_results:
|
||||
search_results["results"] = tv_results
|
||||
else:
|
||||
search_results["results"] = []
|
||||
else:
|
||||
search_results = await self.tmdb_client.search_tv_show(
|
||||
query,
|
||||
language=lang
|
||||
)
|
||||
|
||||
if search_results.get("results"):
|
||||
# Apply year filter if we have one
|
||||
@@ -784,6 +839,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,
|
||||
|
||||
@@ -129,6 +129,9 @@ class SeriesManagerService:
|
||||
if not self.nfo_service:
|
||||
return
|
||||
|
||||
nfo_exists = False
|
||||
ids = {}
|
||||
|
||||
try:
|
||||
folder_path = Path(self.anime_directory) / serie_folder
|
||||
nfo_path = folder_path / "tvshow.nfo"
|
||||
@@ -195,22 +198,49 @@ class SeriesManagerService:
|
||||
logger.info(
|
||||
f"Creating NFO for '{serie_name}' ({serie_folder})"
|
||||
)
|
||||
await self.nfo_service.create_tvshow_nfo(
|
||||
serie_name=serie_name,
|
||||
serie_folder=serie_folder,
|
||||
year=year,
|
||||
download_poster=self.download_poster,
|
||||
download_logo=self.download_logo,
|
||||
download_fanart=self.download_fanart
|
||||
)
|
||||
logger.info("Successfully created NFO for '%s'", serie_name)
|
||||
try:
|
||||
await self.nfo_service.create_tvshow_nfo(
|
||||
serie_name=serie_name,
|
||||
serie_folder=serie_folder,
|
||||
year=year,
|
||||
download_poster=self.download_poster,
|
||||
download_logo=self.download_logo,
|
||||
download_fanart=self.download_fanart
|
||||
)
|
||||
logger.info("Successfully created NFO for '%s'", serie_name)
|
||||
except TMDBAPIError as create_error:
|
||||
# TMDB lookup failed, create minimal NFO to track the series
|
||||
logger.warning(
|
||||
"TMDB lookup failed for '%s', creating minimal NFO: %s",
|
||||
serie_name, create_error
|
||||
)
|
||||
try:
|
||||
await self.nfo_service.create_minimal_nfo(
|
||||
serie_name=serie_name,
|
||||
serie_folder=serie_folder,
|
||||
year=year
|
||||
)
|
||||
logger.info("Created minimal NFO for '%s'", serie_name)
|
||||
except Exception as minimal_error:
|
||||
logger.error(
|
||||
"Failed to create minimal NFO for '%s': %s",
|
||||
serie_name, minimal_error
|
||||
)
|
||||
elif nfo_exists:
|
||||
logger.debug(
|
||||
f"NFO exists for '{serie_name}', skipping download"
|
||||
)
|
||||
|
||||
except TMDBAPIError as e:
|
||||
logger.error("TMDB API error processing '%s': %s", serie_name, e)
|
||||
# Only log at ERROR if no NFO exists and we have no IDs
|
||||
# If NFO exists with IDs, this is just a lookup failure, log at DEBUG
|
||||
if nfo_exists and (ids.get("tmdb_id") or ids.get("tvdb_id")):
|
||||
logger.debug(
|
||||
"TMDB API lookup failed for '%s' (has NFO with IDs): %s",
|
||||
serie_name, e
|
||||
)
|
||||
else:
|
||||
logger.error("TMDB API error processing '%s': %s", serie_name, e)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Unexpected error processing NFO for '{serie_name}': {e}",
|
||||
|
||||
@@ -1809,3 +1809,107 @@ class TestNegativeCache:
|
||||
assert "expired_key" not in tmdb_client._negative_cache
|
||||
assert "valid_key" in tmdb_client._negative_cache
|
||||
|
||||
|
||||
class TestNFOIDOverride:
|
||||
"""Tests for manual TMDB ID override via NFO."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tvshow_nfo_uses_existing_tmdb_id(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de):
|
||||
"""Test that existing TMDB ID in NFO skips search."""
|
||||
# Create series folder with existing NFO containing TMDB ID
|
||||
series_folder = tmp_path / "Attack on Titan"
|
||||
series_folder.mkdir()
|
||||
nfo_path = series_folder / "tvshow.nfo"
|
||||
nfo_path.write_text("""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tvshow>
|
||||
<title>Attack on Titan</title>
|
||||
<tmdbid>1429</tmdbid>
|
||||
</tvshow>
|
||||
""", encoding="utf-8")
|
||||
|
||||
with patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details, \
|
||||
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), \
|
||||
patch.object(nfo_service.tmdb_client, '_ensure_session', new_callable=AsyncMock):
|
||||
|
||||
mock_details.return_value = mock_tmdb_data
|
||||
mock_ratings.return_value = mock_content_ratings_de
|
||||
|
||||
nfo_path_result = await nfo_service.create_tvshow_nfo(
|
||||
"Attack on Titan",
|
||||
"Attack on Titan",
|
||||
download_poster=False, download_logo=False, download_fanart=False
|
||||
)
|
||||
|
||||
# Verify NFO was created
|
||||
assert nfo_path_result.exists()
|
||||
|
||||
# Verify get_tv_show_details was called directly with the ID (no search)
|
||||
mock_details.assert_called_once_with(1429, append_to_response="credits,external_ids,images")
|
||||
|
||||
# Verify search was NOT called
|
||||
# (we can check by verifying no search_tv_show mock was set up)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tvshow_nfo_searches_when_no_tmdb_id(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de):
|
||||
"""Test that search is used when NFO has no TMDB ID."""
|
||||
# Create series folder without existing NFO
|
||||
series_folder = tmp_path / "Test Anime"
|
||||
series_folder.mkdir()
|
||||
|
||||
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, \
|
||||
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), \
|
||||
patch.object(nfo_service.tmdb_client, '_ensure_session', new_callable=AsyncMock):
|
||||
|
||||
mock_search.return_value = {
|
||||
"results": [{
|
||||
"id": 1429,
|
||||
"name": "Test Anime",
|
||||
"first_air_date": "2024-01-01",
|
||||
"overview": "Test overview"
|
||||
}]
|
||||
}
|
||||
mock_details.return_value = mock_tmdb_data
|
||||
mock_ratings.return_value = mock_content_ratings_de
|
||||
|
||||
nfo_path = await nfo_service.create_tvshow_nfo(
|
||||
"Test Anime",
|
||||
"Test Anime",
|
||||
download_poster=False, download_logo=False, download_fanart=False
|
||||
)
|
||||
|
||||
# Verify search was called
|
||||
mock_search.assert_called()
|
||||
|
||||
|
||||
class TestSearchMultiStrategy:
|
||||
"""Tests for search/multi fallback strategy."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_multi_strategy_used_as_fallback(self, nfo_service, mock_tmdb_data):
|
||||
"""Test that search/multi is tried after regular search fails."""
|
||||
mock_search = AsyncMock()
|
||||
mock_multi = AsyncMock()
|
||||
|
||||
# First: regular search fails
|
||||
# Second: multi search returns TV result
|
||||
mock_search.return_value = {"results": []}
|
||||
mock_multi.return_value = {
|
||||
"results": [
|
||||
{"media_type": "movie", "id": 123},
|
||||
{"media_type": "tv", "id": 456, "name": "Found Show", "first_air_date": "2024-01-01"}
|
||||
]
|
||||
}
|
||||
|
||||
with patch.object(nfo_service.tmdb_client, 'search_tv_show', mock_search), \
|
||||
patch.object(nfo_service.tmdb_client, 'search_multi', mock_multi):
|
||||
|
||||
result, source = await nfo_service._search_with_fallback(
|
||||
"Unknown Show", 2024, None
|
||||
)
|
||||
|
||||
assert result["id"] == 456
|
||||
assert source == "multi_search"
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class TestPersistSerieToDbErrorHandling:
|
||||
|
||||
with patch(
|
||||
"src.server.database.connection.get_async_session_factory",
|
||||
return_value=mock_factory
|
||||
return_value=mock_session
|
||||
):
|
||||
with patch(
|
||||
"src.server.database.service.AnimeSeriesService.get_by_key",
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user