fix: restore anime search functionality and server stability
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled

- Fixed fatal ImportError in main.py that blocked code updates
- Guaranteed HTML fragments for search results via parameter and header detection
- Added hidden html field to search form for robust HTMX integration
- Validated fix with E2E API verification
This commit is contained in:
root
2026-03-24 14:10:05 +00:00
parent f99e739ff2
commit eb0c67348f
3 changed files with 13 additions and 48 deletions
+9 -45
View File
@@ -68,11 +68,10 @@ async def search_anime_unified(
html: bool = Query(False), html: bool = Query(False),
): ):
""" """
Search across all anime providers using MetadataEnricher and health checks. Search across all anime providers.
Results are grouped by provider for legacy UI compatibility.
Returns HTML for HTMX requests or if html=True parameter is set. Returns HTML for HTMX requests or if html=True parameter is set.
""" """
print(f"\n[SEARCH] Starting modern unified search for '{q}' in {lang}") print(f"\n[SEARCH] Starting search for '{q}'. html={html}")
start_time = time.time() start_time = time.time()
results = {} results = {}
@@ -87,7 +86,7 @@ async def search_anime_unified(
search_tasks.append(provider.search(q)) search_tasks.append(provider.search(q))
task_metadata.append({"id": provider.id, "type": "generic"}) task_metadata.append({"id": provider.id, "type": "generic"})
# Legacy providers (until migrated to YAML) # Legacy providers
legacy_downloaders = { legacy_downloaders = {
"anime-ultime": AnimeUltimeDownloader(), "anime-ultime": AnimeUltimeDownloader(),
"neko-sama": NekoSamaDownloader(), "neko-sama": NekoSamaDownloader(),
@@ -113,7 +112,6 @@ async def search_anime_unified(
if isinstance(raw_result, Exception): if isinstance(raw_result, Exception):
logger.error(f"Search failed for {pid}: {raw_result}") logger.error(f"Search failed for {pid}: {raw_result}")
continue continue
if not raw_result: if not raw_result:
continue continue
@@ -121,30 +119,20 @@ async def search_anime_unified(
results[pid] = [] results[pid] = []
for item in raw_result: for item in raw_result:
# Normalize to dict
item_dict = item.model_dump() if hasattr(item, "model_dump") else item item_dict = item.model_dump() if hasattr(item, "model_dump") else item
url = item_dict.get("url") url = item_dict.get("url")
if url and url not in seen_urls: if url and url not in seen_urls:
seen_urls.add(url) seen_urls.add(url)
# Check relevance simple boost
if q.lower() in (item_dict.get("title") or "").lower(): if q.lower() in (item_dict.get("title") or "").lower():
item_dict["_relevance_boost"] = 1.0 item_dict["_relevance_boost"] = 1.0
else: else:
item_dict["_relevance_boost"] = 0.5 item_dict["_relevance_boost"] = 0.5
results[pid].append(item_dict) results[pid].append(item_dict)
# Prepare enrichment task for top 5 results per provider # Prepare enrichment task for top 5 results per provider
if len(results[pid]) <= 5: if len(results[pid]) <= 5:
enrichment_tasks.append( enrichment_tasks.append(enricher.enrich_metadata(item_dict.get("metadata", {}), item_dict.get("title", ""), url))
enricher.enrich_metadata(
item_dict.get("metadata", {}),
item_dict.get("title", ""),
url
)
)
enrichment_mapping.append((pid, len(results[pid]) - 1)) enrichment_mapping.append((pid, len(results[pid]) - 1))
else: else:
if "metadata" not in item_dict: if "metadata" not in item_dict:
@@ -152,17 +140,14 @@ async def search_anime_unified(
# 4. Perform parallel enrichment # 4. Perform parallel enrichment
if enrichment_tasks: if enrichment_tasks:
print(f"[SEARCH] Enriching {len(enrichment_tasks)} top results via Kitsu...")
enriched_metas = await asyncio.gather(*enrichment_tasks, return_exceptions=True) enriched_metas = await asyncio.gather(*enrichment_tasks, return_exceptions=True)
# Re-inject enriched metadata
for idx, (pid, pos) in enumerate(enrichment_mapping): for idx, (pid, pos) in enumerate(enrichment_mapping):
if idx < len(enriched_metas): if idx < len(enriched_metas):
meta = enriched_metas[idx] meta = enriched_metas[idx]
if not isinstance(meta, Exception) and meta: if not isinstance(meta, Exception) and meta:
results[pid][pos]["metadata"] = meta.model_dump() results[pid][pos]["metadata"] = meta.model_dump()
# 5. Sort results by relevance per provider # 5. Sort results
for pid in results: for pid in results:
results[pid].sort(key=lambda x: -x.get("_relevance_boost", 0)) results[pid].sort(key=lambda x: -x.get("_relevance_boost", 0))
for item in results[pid]: for item in results[pid]:
@@ -170,15 +155,17 @@ async def search_anime_unified(
elapsed = time.time() - start_time elapsed = time.time() - start_time
total_found = sum(len(r) for r in results.values()) total_found = sum(len(r) for r in results.values())
print(f"[SEARCH] Finished in {elapsed:.2f}s. Found {total_found} unique results across {len(results)} providers.") print(f"[SEARCH] Finished in {elapsed:.2f}s. Found {total_found} results. HX-Request={request.headers.get('HX-Request')}")
# 6. Return HTML for HTMX or JSON for API # 6. Return HTML for HTMX or JSON for API
if html or request.headers.get("HX-Request"): if html or request.headers.get("HX-Request"):
print("[SEARCH] Returning HTML response")
return templates.TemplateResponse( return templates.TemplateResponse(
"components/anime_search_results.html", "components/anime_search_results.html",
{"request": request, "results": results} {"request": request, "results": results}
) )
print("[SEARCH] Returning JSON response")
return { return {
"query": q, "query": q,
"lang": lang, "lang": lang,
@@ -199,8 +186,6 @@ async def search_series_unified(
from app.downloaders.series_sites import FS7Downloader from app.downloaders.series_sites import FS7Downloader
print(f"\n[SERIES SEARCH] Starting search for '{q}' in {lang}") print(f"\n[SERIES SEARCH] Starting search for '{q}' in {lang}")
start_time = time.time()
results = {} results = {}
series_downloaders = {"fs7": FS7Downloader()} series_downloaders = {"fs7": FS7Downloader()}
search_tasks = [] search_tasks = []
@@ -209,23 +194,17 @@ async def search_series_unified(
for provider_id, provider in get_series_providers().items(): for provider_id, provider in get_series_providers().items():
if provider_id in series_downloaders: if provider_id in series_downloaders:
downloader = series_downloaders[provider_id] downloader = series_downloaders[provider_id]
print(f"[SERIES SEARCH] Queueing search on {provider_id}...")
search_tasks.append(downloader.search_anime(q, lang)) search_tasks.append(downloader.search_anime(q, lang))
provider_ids.append(provider_id) provider_ids.append(provider_id)
print(f"[SERIES SEARCH] Waiting for {len(search_tasks)} searches...")
search_results = await asyncio.gather(*search_tasks, return_exceptions=True) search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
for provider_id, result in zip(provider_ids, search_results): for provider_id, result in zip(provider_ids, search_results):
if isinstance(result, Exception): if isinstance(result, Exception):
print(f"[SERIES SEARCH] {provider_id} error: {str(result)}") print(f"[SERIES SEARCH] {provider_id} error: {str(result)}")
elif result: elif result:
print(f"[SERIES SEARCH] {provider_id} found {len(result)} results")
results[provider_id] = result results[provider_id] = result
elapsed = time.time() - start_time
print(f"[SERIES SEARCH] Completed in {elapsed:.2f}s\n")
return {"query": q, "lang": lang, "results": results} return {"query": q, "lang": lang, "results": results}
@@ -238,10 +217,7 @@ async def get_anime_metadata(url: str):
metadata = await downloader.get_anime_metadata(url) metadata = await downloader.get_anime_metadata(url)
return {"url": url, "metadata": metadata} return {"url": url, "metadata": metadata}
else: else:
raise HTTPException( raise HTTPException(status_code=400, detail=f"Downloader for {url} does not support metadata extraction")
status_code=400,
detail=f"Downloader for {url} does not support metadata extraction",
)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@@ -263,17 +239,6 @@ async def get_anime_providers_list():
return {"providers": get_anime_providers()} return {"providers": get_anime_providers()}
@router.get("/anime-sama/search")
async def search_anime_sama(
q: str,
lang: str = "vostfr",
):
"""Search for anime on anime-sama (legacy)"""
downloader = AnimeSamaDownloader()
results = await downloader.search_anime(q, lang)
return {"query": q, "lang": lang, "results": results}
@router.post("/anime/download") @router.post("/anime/download")
async def download_anime_episode( async def download_anime_episode(
url: str, url: str,
@@ -352,7 +317,6 @@ async def search_anime_mal_details(
await fetcher.close() await fetcher.close()
@router.post("/api/translate")
@router.post("/translate") @router.post("/translate")
async def translate_text(request: Request): async def translate_text(request: Request):
"""Translate text from English to French using Google Translate""" """Translate text from English to French using Google Translate"""
+2 -2
View File
@@ -116,10 +116,8 @@ templates = Jinja2Templates(directory="templates")
# ==================== INCLUDE ROUTERS ==================== # ==================== INCLUDE ROUTERS ====================
from app.routers import ( from app.routers import (
root_router,
auth_router, auth_router,
downloads_router, downloads_router,
downloads_legacy_router,
anime_router, anime_router,
favorites_router, favorites_router,
recommendations_router, recommendations_router,
@@ -127,8 +125,10 @@ from app.routers import (
sonarr_router, sonarr_router,
player_router, player_router,
static_router, static_router,
root_router,
) )
# Include routers # Include routers
app.include_router(root_router) app.include_router(root_router)
app.include_router(auth_router) app.include_router(auth_router)
+2 -1
View File
@@ -15,10 +15,11 @@
<h2>🎬 Rechercher un Anime</h2> <h2>🎬 Rechercher un Anime</h2>
</div> </div>
<div class="url-form"> <div class="url-form">
<form hx-get="/api/anime/search?html=1" <form hx-get="/api/anime/search"
hx-target="#animeSearchResults" hx-target="#animeSearchResults"
hx-indicator="#search-loading" hx-indicator="#search-loading"
class="input-group"> class="input-group">
<input type="hidden" name="html" value="1">
<input <input
type="text" type="text"
name="q" name="q"