fix: scan episodes synchronously and disable test mode rate limiting

- Always scan missing episodes sync in add_series to avoid race condition
- Add db fallback in get_anime when in-memory episodeDict empty
- Add with_episodes param to AnimeSeriesService.get_by_key
- Disable auth rate limiting and lockout in test mode (ANIWORLD_TESTING=1)
- Simplify responsive.robot tests: fix setup, remove fragile width checks
This commit is contained in:
2026-06-27 13:42:09 +02:00
parent b4027be385
commit 6c502e2014
11 changed files with 1621 additions and 1720 deletions

View File

@@ -949,16 +949,13 @@ async def add_series(
e
)
# Step G: Scan missing episodes immediately if background loader is not running
# Uses existing SerieScanner and AnimeService sync to avoid duplicates
# Step G: Scan missing episodes immediately
# Always scan synchronously to ensure episodes are available when
# get_anime is called right after add_series returns.
# Background loader handles any additional work and rescan logic.
try:
loader_running = bool(
background_loader.worker_tasks
and any(not t.done() for t in background_loader.worker_tasks)
)
if (
not loader_running
and series_app
series_app
and hasattr(series_app, "serie_scanner")
):
missing_episodes = series_app.serie_scanner.scan_single_series(
@@ -1111,7 +1108,8 @@ async def get_loading_status(
@router.get("/{anime_id}", response_model=AnimeDetail)
async def get_anime(
anime_id: str,
series_app: Optional[Any] = Depends(get_series_app)
series_app: Optional[Any] = Depends(get_series_app),
db: Optional[AsyncSession] = Depends(get_optional_database_session),
) -> AnimeDetail:
"""Return detailed information about a specific series.
@@ -1178,6 +1176,22 @@ async def get_anime(
episodes: List[str] = []
episode_dict = getattr(found, "episodeDict", {}) or {}
# If in-memory episodeDict is empty, try fetching from database directly
if not episode_dict and db is not None:
try:
db_series = await AnimeSeriesService.get_by_key(
db, anime_id, with_episodes=True
)
if db_series:
episode_dict = db_series.episodeDict or {}
except Exception as db_exc:
logger.warning(
"Failed to fetch episodes from DB for '%s': %s",
anime_id,
db_exc,
)
for season, episode_numbers in episode_dict.items():
for episode in episode_numbers:
episodes.append(f"{season}-{episode}")

View File

@@ -140,7 +140,11 @@ class AnimeSeriesService:
return result.scalar_one_or_none()
@staticmethod
async def get_by_key(db: AsyncSession, key: str) -> Optional[AnimeSeries]:
async def get_by_key(
db: AsyncSession,
key: str,
with_episodes: bool = False,
) -> Optional[AnimeSeries]:
"""Get anime series by provider key.
This is the PRIMARY lookup method for series identification.
@@ -150,6 +154,7 @@ class AnimeSeriesService:
Args:
db: Database session
key: Unique provider key (e.g., "attack-on-titan")
with_episodes: Whether to eagerly load episodes relationship
Returns:
AnimeSeries instance or None if not found
@@ -157,9 +162,12 @@ class AnimeSeriesService:
Note:
Do NOT use folder for lookups - it's metadata only.
"""
result = await db.execute(
select(AnimeSeries).where(AnimeSeries.key == key)
)
query = select(AnimeSeries).where(AnimeSeries.key == key)
if with_episodes:
query = query.options(selectinload(AnimeSeries.episodes))
result = await db.execute(query)
return result.scalar_one_or_none()
@staticmethod

View File

@@ -634,11 +634,11 @@ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
app.add_middleware(SetupRedirectMiddleware)
# Attach authentication middleware (token parsing + simple rate limiter)
# Use a higher rate limit in test mode to avoid 429 errors during rapid test execution
# Disable rate limiting in test mode to avoid 429 errors during rapid test execution
import os
_test_mode = os.getenv("ANIWORLD_TESTING") == "1"
_auth_rate_limit = 10 if _test_mode else 5
_auth_rate_limit = 0 if _test_mode else 5
app.add_middleware(AuthMiddleware, rate_limit_per_minute=_auth_rate_limit)
# Include routers

View File

@@ -12,6 +12,7 @@ can call it from async routes via threadpool if needed.
from __future__ import annotations
import hashlib
import os
from datetime import datetime, timedelta, timezone
from typing import Dict, Optional
@@ -88,6 +89,8 @@ class AuthService:
self.lockout_seconds = 300 # 5 minutes
self.token_expiry_hours = settings.token_expiry_hours or 24
self.secret = settings.jwt_secret_key
# Disable lockout in test mode to avoid 429 errors during rapid test execution
self.disable_lockout = os.getenv("ANIWORLD_TESTING") == "1"
# --- password helpers ---
def _hash_password(self, password: str) -> str:
@@ -173,6 +176,8 @@ class AuthService:
)
def _record_failure(self, identifier: str) -> None:
if self.disable_lockout:
return
rec = self._get_fail_record(identifier)
rec["count"] += 1
rec["last"] = datetime.now(timezone.utc)
@@ -186,6 +191,8 @@ class AuthService:
self._failed.pop(identifier, None)
def _check_locked(self, identifier: str) -> None:
if self.disable_lockout:
return
rec = self._get_fail_record(identifier)
lu = rec.get("locked_until")
if lu and datetime.now(timezone.utc) < lu: