anime / app.py
Neon-AI's picture
Update app.py
a5af68f verified
from fastapi import FastAPI, HTTPException
import requests
from bs4 import BeautifulSoup
import re
app = FastAPI(title="Neon Anime Api")
@app.get("/")
def home():
return {"status": "Neon Anime Api Alive"}
def normalize_text(text: str) -> str:
# Lowercase everything
text = text.lower()
# Remove all characters except letters, numbers, and spaces
text = re.sub(r"[^a-z0-9\s]", "", text)
# Collapse multiple spaces into one
text = re.sub(r"\s+", " ", text)
# Trim spaces at the start and end
return text.strip()
@app.get("/search")
def search_anime(keyword: str):
try:
# Normalize the keyword
normalized_keyword = normalize_text(keyword)
url = f"https://www.aniwave.se/filter?keyword={normalized_keyword}"
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(url, headers=headers)
if resp.status_code != 200:
raise HTTPException(status_code=500, detail="Failed to fetch search page")
soup = BeautifulSoup(resp.text, "html.parser")
items = soup.select("div.item")
results = []
for item in items:
title_elem = item.select_one("a.name.d-title")
title = title_elem.text.strip() if title_elem else None
path = title_elem["href"] if title_elem else None
thumb_elem = item.select_one("div.ani.poster img")
thumbnail = thumb_elem.get("src") if thumb_elem else None
sub_elem = item.select_one("span.ep-status.sub span")
dub_elem = item.select_one("span.ep-status.dub span")
total_elem = item.select_one("span.ep-status.total span")
episodes = {
"sub": int(sub_elem.text.strip()) if sub_elem else None,
"dub": int(dub_elem.text.strip()) if dub_elem else None,
"total": int(total_elem.text.strip()) if total_elem else None
}
type_elem = item.select_one("div.meta .inner .right")
anime_type = type_elem.text.strip() if type_elem else None
if title and path:
results.append({
"title": title,
"url": path,
"thumbnail": thumbnail,
"episodes": episodes,
"type": anime_type
})
# Custom ranking
keyword_lower = keyword.lower()
def rank(item):
t = item["title"].lower()
if t == keyword_lower:
return 0 # exact match first
elif t.startswith(keyword_lower):
return 1 # starts with keyword second
else:
return 2 # the rest
results = sorted(results, key=lambda x: (rank(x), x["title"].lower()))
return {"keyword": keyword, "results": results}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to search anime: {str(e)}")
@app.get("/metadata")
def get_metadata(path: str):
"""
Fetch full anime metadata from AniWave with proper fields.
"""
try:
full_url = f"https://www.aniwave.se{path}"
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(full_url, headers=headers)
if resp.status_code != 200:
raise HTTPException(status_code=500, detail="Failed to fetch anime page")
soup = BeautifulSoup(resp.text, "html.parser")
# Thumbnail
thumb_elem = soup.select_one("img[itemprop='image']")
thumbnail = thumb_elem["src"] if thumb_elem else None
# Title
title_elem = soup.select_one(".names.font-italic.mb-2")
title = title_elem.text.strip() if title_elem else None
# Rating / Quality / Sub / Dub
rating_elem = soup.select_one(".meta.icons .rating")
rating = rating_elem.text.strip() if rating_elem else None
quality_elem = soup.select_one(".meta.icons .quality")
quality = quality_elem.text.strip() if quality_elem else None
has_sub = bool(soup.select_one(".meta.icons .sub"))
has_dub = bool(soup.select_one(".meta.icons .dub"))
# Description
desc_elem = soup.select_one(".synopsis .content")
description = desc_elem.text.strip() if desc_elem else None
# bmeta fields
bmeta_divs = soup.select(".bmeta .meta")
type_ = source = premiered = date_aired = broadcast = status = mal_rating = duration = total_episodes = None
genres = []
studios = []
producers = []
if bmeta_divs:
for div in bmeta_divs:
for item in div.find_all("div", recursive=False):
text = item.get_text(separator="|", strip=True)
if text.startswith("Type:"):
type_elem = item.find("a")
type_ = type_elem.text.strip() if type_elem else None
elif text.startswith("Source:"):
source_elem = item.find("span")
source = source_elem.text.strip() if source_elem else None
elif text.startswith("Premiered:"):
premiered_elem = item.find("a")
premiered = premiered_elem.text.strip() if premiered_elem else None
elif text.startswith("Date aired:"):
date_elem = item.find("span")
date_aired = date_elem.text.strip() if date_elem else None
elif text.startswith("Broadcast:"):
broadcast = item.text.replace("Broadcast:", "").strip()
elif text.startswith("Status:"):
status_elem = item.find("a")
status = status_elem.text.strip() if status_elem else None
elif text.startswith("Genres:"):
genres = [g.text.strip() for g in item.find_all("a")]
elif text.startswith("MAL:"):
raw = item.get_text(strip=True) # e.g., "MAL: 7.97 by 1786345 reviews"
match = re.search(r"MAL:\s*([\d.]+)", raw)
mal_rating = match.group(1) if match else None
elif text.startswith("Duration:"):
duration = item.text.replace("Duration:", "").strip()
elif text.startswith("Episodes:"):
ep_elem = item.find("span")
total_episodes = ep_elem.text.strip() if ep_elem else None
elif text.startswith("Studios:"):
studios = list({s.text.strip() for s in soup.select("div.meta div:contains('Studios:') a")})
elif text.startswith("Producers:"):
producers = list({p.text.strip() for p in soup.select("div.meta div:contains('Producers:') a")})
return {
"title": title,
"rating": rating,
"quality": quality,
"has_sub": has_sub,
"has_dub": has_dub,
"description": description,
"type": type_,
"source": source,
"premiered": premiered,
"date_aired": date_aired,
"broadcast": broadcast,
"status": status,
"genres": genres,
"mal_rating": mal_rating,
"duration": duration,
"total_episodes": total_episodes,
"studios": studios,
"producers": producers,
"thumbnail": thumbnail,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to fetch metadata: {str(e)}")
# ===== END PATCH =====
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 7860)) # HF Spaces sets this automatically
uvicorn.run(app, host="0.0.0.0", port=port)