feat: fix search, poster serving, and add hover overlay UI for cards

- Fix search store exports to match component expectations (inputValue,
  bangumiList, onSearch) and transform data to SearchResult format
- Fix poster endpoint path check that incorrectly blocked all requests
- Add resolvePosterUrl utility to handle both external URLs and local paths
- Move tags into hover overlay on homepage cards and calendar cards
- Show title and tags on poster hover with dark semi-transparent styling
- Add downloader API, store, and page
- Update backend to async patterns and uv migration changes
- Remove .claude/settings.local.json from tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Estrella Pan
2026-01-23 21:20:12 +01:00
parent 0408ecdd61
commit a98a162500
52 changed files with 2269 additions and 1727 deletions

View File

@@ -7,13 +7,13 @@ logger = logging.getLogger(__name__)
BGM_CALENDAR_URL = "https://api.bgm.tv/calendar"
def fetch_bgm_calendar() -> list[dict]:
async def fetch_bgm_calendar() -> list[dict]:
"""Fetch the current season's broadcast calendar from Bangumi.tv API.
Returns a flat list of anime items with their air_weekday (0=Mon, ..., 6=Sun).
"""
with RequestContent() as req:
data = req.get_json(BGM_CALENDAR_URL)
async with RequestContent() as req:
data = await req.get_json(BGM_CALENDAR_URL)
if not data:
logger.warning("[BGM Calendar] Failed to fetch calendar data.")

View File

@@ -5,10 +5,10 @@ def search_url(e):
return f"https://api.bgm.tv/search/subject/{e}?responseGroup=large"
def bgm_parser(title):
async def bgm_parser(title):
url = search_url(title)
with RequestContent() as req:
contents = req.get_json(url)
async with RequestContent() as req:
contents = await req.get_json(url)
if contents:
return contents[0]
else:

View File

@@ -7,10 +7,10 @@ from module.network import RequestContent
from module.utils import save_image
def mikan_parser(homepage: str):
async def mikan_parser(homepage: str):
root_path = parse_url(homepage).host
with RequestContent() as req:
content = req.get_html(homepage)
async with RequestContent() as req:
content = await req.get_html(homepage)
soup = BeautifulSoup(content, "html.parser")
poster_div = soup.find("div", {"class": "bangumi-poster"}).get("style")
official_title = soup.select_one(
@@ -20,7 +20,7 @@ def mikan_parser(homepage: str):
if poster_div:
poster_path = poster_div.split("url('")[1].split("')")[0]
poster_path = poster_path.split("?")[0]
img = req.get_content(f"https://{root_path}{poster_path}")
img = await req.get_content(f"https://{root_path}{poster_path}")
suffix = poster_path.split(".")[-1]
poster_link = save_image(img, suffix)
return poster_link, official_title
@@ -28,5 +28,6 @@ def mikan_parser(homepage: str):
if __name__ == '__main__':
import asyncio
homepage = "https://mikanani.me/Home/Episode/c89b3c6f0c1c0567a618f5288b853823c87a9862"
print(mikan_parser(homepage))
print(asyncio.run(mikan_parser(homepage)))

View File

@@ -31,11 +31,11 @@ def info_url(e, key):
return f"{TMDB_URL}/3/tv/{e}?api_key={TMDB_API}&language={LANGUAGE[key]}"
def is_animation(tv_id, language) -> bool:
async def is_animation(tv_id, language, req: RequestContent) -> bool:
url_info = info_url(tv_id, language)
with RequestContent() as req:
type_id = req.get_json(url_info)["genres"]
for type in type_id:
type_id = await req.get_json(url_info)
if type_id:
for type in type_id.get("genres", []):
if type.get("id") == 16:
return True
return False
@@ -56,21 +56,27 @@ def get_season(seasons: list) -> tuple[int, str]:
return len(ss), ss[-1].get("poster_path")
def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None:
with RequestContent() as req:
async def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None:
async with RequestContent() as req:
url = search_url(title)
contents = req.get_json(url).get("results")
contents = await req.get_json(url)
if not contents:
return None
contents = contents.get("results")
if contents.__len__() == 0:
url = search_url(title.replace(" ", ""))
contents = req.get_json(url).get("results")
contents_resp = await req.get_json(url)
if not contents_resp:
return None
contents = contents_resp.get("results")
# 判断动画
if contents:
for content in contents:
id = content["id"]
if is_animation(id, language):
if await is_animation(id, language, req):
break
url_info = info_url(id, language)
info_content = req.get_json(url_info)
info_content = await req.get_json(url_info)
season = [
{
"season": s.get("name"),
@@ -87,7 +93,7 @@ def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None:
year_number = info_content.get("first_air_date").split("-")[0]
if poster_path:
if not test:
img = req.get_content(f"https://image.tmdb.org/t/p/w780{poster_path}")
img = await req.get_content(f"https://image.tmdb.org/t/p/w780{poster_path}")
poster_link = save_image(img, "jpg")
else:
poster_link = "https://image.tmdb.org/t/p/w780" + poster_path
@@ -107,4 +113,5 @@ def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None:
if __name__ == "__main__":
print(tmdb_parser("魔法禁书目录", "zh"))
import asyncio
print(asyncio.run(tmdb_parser("魔法禁书目录", "zh")))