fix: restore anime search functionality and server stability
- 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:
@@ -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"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user