feat: fix auth, provider health checks, search, and redesign UI
- Fix register/login: dict-style access on UserTable ORM objects - Fix HTMX auth: inject JWT token in all HTMX request headers - Fix FS7 search: use DLE AJAX endpoint /engine/ajax/search.php - Fix ZT search: use ?p=series&search=QUERY (not DLE format) - Fix provider health: load hardcoded providers + domain manager - Add self.id to all anime/series providers - Redesign homepage: Netflix-style horizontal scroll cards (.hc) - Redesign search results: grouped by title, poster + synopsis + 3 buttons - Add Télécharger dropdown: season download + episode picker - Fix navbar CSS: restore .tabs flex layout, remove orphan rules - Fix HTMX spinner: remove inline display:none, use CSS indicator - Add AGENTS.md files across project for developer documentation
This commit is contained in:
+89
-13
@@ -1,4 +1,5 @@
|
||||
"""Manages scraper providers and their health status"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import asyncio
|
||||
@@ -7,6 +8,18 @@ from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from app.downloaders.generic_scraper import GenericScraper
|
||||
from app.downloaders.anime_sites import (
|
||||
AnimeSamaDownloader,
|
||||
NekoSamaDownloader,
|
||||
AnimeUltimeDownloader,
|
||||
VostfreeDownloader,
|
||||
FrenchMangaDownloader,
|
||||
)
|
||||
from app.downloaders.series_sites import (
|
||||
FS7Downloader,
|
||||
ZoneTelechargementDownloader,
|
||||
)
|
||||
from app.providers import ANIME_PROVIDERS, SERIES_PROVIDERS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,11 +29,13 @@ class ProvidersManager:
|
||||
|
||||
def __init__(self, config_dir: str = "app/downloaders/providers_config"):
|
||||
self.config_dir = Path(config_dir)
|
||||
self.providers: Dict[str, GenericScraper] = {}
|
||||
self.providers: Dict[str, object] = {}
|
||||
self.provider_info: Dict[str, Dict] = {}
|
||||
self.health_status: Dict[str, Dict] = {}
|
||||
self._load_providers()
|
||||
self._load_yaml_providers()
|
||||
self._load_hardcoded_providers()
|
||||
|
||||
def _load_providers(self):
|
||||
def _load_yaml_providers(self):
|
||||
"""Load all providers from YAML configs"""
|
||||
if not self.config_dir.exists():
|
||||
logger.warning(f"Providers config directory not found: {self.config_dir}")
|
||||
@@ -33,46 +48,107 @@ class ProvidersManager:
|
||||
self.health_status[scraper.id] = {
|
||||
"status": "unknown",
|
||||
"last_check": None,
|
||||
"error": None
|
||||
"error": None,
|
||||
}
|
||||
logger.info(f"Loaded provider: {scraper.name} ({scraper.id})")
|
||||
logger.info(f"Loaded YAML provider: {scraper.name} ({scraper.id})")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load provider from {config_file}: {e}")
|
||||
|
||||
def _load_hardcoded_providers(self):
|
||||
"""Load hardcoded Python providers"""
|
||||
provider_classes = [
|
||||
("anime-sama", AnimeSamaDownloader, ANIME_PROVIDERS),
|
||||
("neko-sama", NekoSamaDownloader, ANIME_PROVIDERS),
|
||||
("anime-ultime", AnimeUltimeDownloader, ANIME_PROVIDERS),
|
||||
("vostfree", VostfreeDownloader, ANIME_PROVIDERS),
|
||||
("french-manga", FrenchMangaDownloader, ANIME_PROVIDERS),
|
||||
("fs7", FS7Downloader, SERIES_PROVIDERS),
|
||||
("zonetelechargement", ZoneTelechargementDownloader, SERIES_PROVIDERS),
|
||||
]
|
||||
|
||||
for provider_id, provider_class, provider_dict in provider_classes:
|
||||
if provider_id in provider_dict:
|
||||
try:
|
||||
self.providers[provider_id] = provider_class()
|
||||
self.provider_info[provider_id] = provider_dict[provider_id]
|
||||
self.health_status[provider_id] = {
|
||||
"status": "unknown",
|
||||
"last_check": None,
|
||||
"error": None,
|
||||
}
|
||||
logger.info(f"Loaded hardcoded provider: {provider_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load provider {provider_id}: {e}")
|
||||
|
||||
async def check_all_health(self):
|
||||
"""Check health of all registered providers"""
|
||||
logger.info("Checking health of all providers...")
|
||||
tasks = []
|
||||
for provider_id, scraper in self.providers.items():
|
||||
tasks.append(self._check_single_health(provider_id, scraper))
|
||||
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
logger.info("Provider health check complete")
|
||||
|
||||
async def _check_single_health(self, provider_id: str, scraper: GenericScraper):
|
||||
async def _check_single_health(self, provider_id: str, scraper):
|
||||
"""Check health of a single provider and update status"""
|
||||
try:
|
||||
is_healthy = await scraper.check_health()
|
||||
is_healthy = await self._do_health_check(scraper)
|
||||
self.health_status[provider_id] = {
|
||||
"status": "up" if is_healthy else "down",
|
||||
"last_check": datetime.now().isoformat(),
|
||||
"error": None if is_healthy else "No search results returned"
|
||||
"error": None if is_healthy else "No search results returned",
|
||||
}
|
||||
except Exception as e:
|
||||
self.health_status[provider_id] = {
|
||||
"status": "down",
|
||||
"last_check": datetime.now().isoformat(),
|
||||
"error": str(e)
|
||||
"error": str(e),
|
||||
}
|
||||
logger.error(f"Health check failed for {provider_id}: {e}")
|
||||
|
||||
def get_provider(self, provider_id: str) -> Optional[GenericScraper]:
|
||||
async def _do_health_check(self, scraper) -> bool:
|
||||
"""Perform health check on a scraper"""
|
||||
try:
|
||||
if hasattr(scraper, "check_health"):
|
||||
return await scraper.check_health()
|
||||
elif hasattr(scraper, "client"):
|
||||
# Test basic connectivity
|
||||
base_url = getattr(scraper, "base_url", None) or getattr(
|
||||
scraper, "active_url", None
|
||||
)
|
||||
if base_url:
|
||||
if hasattr(scraper, "_ensure_base_url"):
|
||||
await scraper._ensure_base_url()
|
||||
base_url = getattr(scraper, "base_url", base_url)
|
||||
response = await scraper.client.get(base_url, timeout=15.0)
|
||||
return 200 <= response.status_code < 400
|
||||
elif hasattr(scraper, "BASE_DOMAINS") and scraper.BASE_DOMAINS:
|
||||
# Test first domain from BASE_DOMAINS
|
||||
test_url = f"https://{scraper.BASE_DOMAINS[0]}"
|
||||
response = await scraper.client.get(test_url, timeout=15.0)
|
||||
return 200 <= response.status_code < 400
|
||||
elif hasattr(scraper, "search_anime"):
|
||||
results = await scraper.search_anime("One Piece", lang="vostfr")
|
||||
return len(results) > 0
|
||||
elif hasattr(scraper, "search"):
|
||||
results = await scraper.search("One Piece")
|
||||
return len(results) > 0
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Health check exception for {getattr(scraper, 'provider_id', scraper)}: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
def get_provider(self, provider_id: str):
|
||||
return self.providers.get(provider_id)
|
||||
|
||||
def get_active_providers(self) -> List[GenericScraper]:
|
||||
def get_active_providers(self) -> List:
|
||||
"""Return only providers that are UP or UNKNOWN"""
|
||||
return [
|
||||
self.providers[pid] for pid, status in self.health_status.items()
|
||||
self.providers[pid]
|
||||
for pid, status in self.health_status.items()
|
||||
if status["status"] != "down"
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user