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:
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user