Spaces:
Paused
Paused
File size: 7,911 Bytes
98cc4cf ddd0050 fa75c8d c21e3cf 98cc4cf f54e46e c21e3cf f54e46e 668030e bf5c2f2 668030e 086c0f9 ee72fb7 086c0f9 ddd0050 086c0f9 a3aad60 086c0f9 a3aad60 fbd240a a3aad60 fbd240a a3aad60 451e011 a3aad60 451e011 bf5c2f2 ee72fb7 451e011 bf5c2f2 f92fb28 30a1d76 1897155 f92fb28 6d71073 f92fb28 c21e3cf f92fb28 30a1d76 f92fb28 c21e3cf 1897155 c21e3cf 1897155 c21e3cf 30a1d76 c21e3cf 1897155 6d71073 fa75c8d 6d71073 fa75c8d 6d71073 fa75c8d 23b1854 f92fb28 30a1d76 c21e3cf 6d71073 c21e3cf 30a1d76 c21e3cf f92fb28 30a1d76 bf5c2f2 f54e46e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | 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) |