mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-04-23 18:11:37 +08:00
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:
@@ -2,9 +2,8 @@ import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.sql import func
|
||||
from sqlmodel import and_, delete, false, or_, select
|
||||
from sqlmodel import Session, and_, delete, false, or_, select
|
||||
|
||||
from module.models import Bangumi, BangumiUpdate
|
||||
|
||||
@@ -23,32 +22,32 @@ def _invalidate_bangumi_cache():
|
||||
|
||||
|
||||
class BangumiDatabase:
|
||||
def __init__(self, session: AsyncSession):
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
async def add(self, data: Bangumi) -> bool:
|
||||
def add(self, data: Bangumi) -> bool:
|
||||
statement = select(Bangumi).where(Bangumi.title_raw == data.title_raw)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
bangumi = result.scalar_one_or_none()
|
||||
if bangumi:
|
||||
return False
|
||||
self.session.add(data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Insert {data.official_title} into database.")
|
||||
return True
|
||||
|
||||
async def add_all(self, datas: list[Bangumi]):
|
||||
def add_all(self, datas: list[Bangumi]):
|
||||
self.session.add_all(datas)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Insert {len(datas)} bangumi into database.")
|
||||
|
||||
async def update(self, data: Bangumi | BangumiUpdate, _id: int = None) -> bool:
|
||||
def update(self, data: Bangumi | BangumiUpdate, _id: int = None) -> bool:
|
||||
if _id and isinstance(data, BangumiUpdate):
|
||||
db_data = await self.session.get(Bangumi, _id)
|
||||
db_data = self.session.get(Bangumi, _id)
|
||||
elif isinstance(data, Bangumi):
|
||||
db_data = await self.session.get(Bangumi, data.id)
|
||||
db_data = self.session.get(Bangumi, data.id)
|
||||
else:
|
||||
return False
|
||||
if not db_data:
|
||||
@@ -57,70 +56,70 @@ class BangumiDatabase:
|
||||
for key, value in bangumi_data.items():
|
||||
setattr(db_data, key, value)
|
||||
self.session.add(db_data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Update {data.official_title}")
|
||||
return True
|
||||
|
||||
async def update_all(self, datas: list[Bangumi]):
|
||||
def update_all(self, datas: list[Bangumi]):
|
||||
self.session.add_all(datas)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Update {len(datas)} bangumi.")
|
||||
|
||||
async def update_rss(self, title_raw: str, rss_set: str):
|
||||
def update_rss(self, title_raw: str, rss_set: str):
|
||||
statement = select(Bangumi).where(Bangumi.title_raw == title_raw)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
bangumi = result.scalar_one_or_none()
|
||||
if bangumi:
|
||||
bangumi.rss_link = rss_set
|
||||
bangumi.added = False
|
||||
self.session.add(bangumi)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Update {title_raw} rss_link to {rss_set}.")
|
||||
|
||||
async def update_poster(self, title_raw: str, poster_link: str):
|
||||
def update_poster(self, title_raw: str, poster_link: str):
|
||||
statement = select(Bangumi).where(Bangumi.title_raw == title_raw)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
bangumi = result.scalar_one_or_none()
|
||||
if bangumi:
|
||||
bangumi.poster_link = poster_link
|
||||
self.session.add(bangumi)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Update {title_raw} poster_link to {poster_link}.")
|
||||
|
||||
async def delete_one(self, _id: int):
|
||||
def delete_one(self, _id: int):
|
||||
statement = select(Bangumi).where(Bangumi.id == _id)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
bangumi = result.scalar_one_or_none()
|
||||
if bangumi:
|
||||
await self.session.delete(bangumi)
|
||||
await self.session.commit()
|
||||
self.session.delete(bangumi)
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Delete bangumi id: {_id}.")
|
||||
|
||||
async def delete_all(self):
|
||||
def delete_all(self):
|
||||
statement = delete(Bangumi)
|
||||
await self.session.execute(statement)
|
||||
await self.session.commit()
|
||||
self.session.execute(statement)
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
|
||||
async def search_all(self) -> list[Bangumi]:
|
||||
def search_all(self) -> list[Bangumi]:
|
||||
global _bangumi_cache, _bangumi_cache_time
|
||||
now = time.time()
|
||||
if _bangumi_cache is not None and (now - _bangumi_cache_time) < _BANGUMI_CACHE_TTL:
|
||||
return _bangumi_cache
|
||||
statement = select(Bangumi)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
_bangumi_cache = list(result.scalars().all())
|
||||
_bangumi_cache_time = now
|
||||
return _bangumi_cache
|
||||
|
||||
async def search_id(self, _id: int) -> Optional[Bangumi]:
|
||||
def search_id(self, _id: int) -> Optional[Bangumi]:
|
||||
statement = select(Bangumi).where(Bangumi.id == _id)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
bangumi = result.scalar_one_or_none()
|
||||
if bangumi is None:
|
||||
logger.warning(f"[Database] Cannot find bangumi id: {_id}.")
|
||||
@@ -129,19 +128,19 @@ class BangumiDatabase:
|
||||
logger.debug(f"[Database] Find bangumi id: {_id}.")
|
||||
return bangumi
|
||||
|
||||
async def match_poster(self, bangumi_name: str) -> str:
|
||||
def match_poster(self, bangumi_name: str) -> str:
|
||||
statement = select(Bangumi).where(
|
||||
func.instr(bangumi_name, Bangumi.official_title) > 0
|
||||
)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
data = result.scalar_one_or_none()
|
||||
if data:
|
||||
return data.poster_link
|
||||
else:
|
||||
return ""
|
||||
|
||||
async def match_list(self, torrent_list: list, rss_link: str) -> list:
|
||||
match_datas = await self.search_all()
|
||||
def match_list(self, torrent_list: list, rss_link: str) -> list:
|
||||
match_datas = self.search_all()
|
||||
if not match_datas:
|
||||
return torrent_list
|
||||
# Build index for faster lookup
|
||||
@@ -162,29 +161,29 @@ class BangumiDatabase:
|
||||
unmatched.append(torrent)
|
||||
# Batch commit all rss_link updates
|
||||
if rss_updated:
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
_invalidate_bangumi_cache()
|
||||
logger.debug(f"[Database] Batch updated rss_link for {len(rss_updated)} bangumi.")
|
||||
return unmatched
|
||||
|
||||
async def match_torrent(self, torrent_name: str) -> Optional[Bangumi]:
|
||||
def match_torrent(self, torrent_name: str) -> Optional[Bangumi]:
|
||||
statement = select(Bangumi).where(
|
||||
and_(
|
||||
func.instr(torrent_name, Bangumi.title_raw) > 0,
|
||||
Bangumi.deleted == false(),
|
||||
)
|
||||
)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def not_complete(self) -> list[Bangumi]:
|
||||
def not_complete(self) -> list[Bangumi]:
|
||||
condition = select(Bangumi).where(
|
||||
and_(Bangumi.eps_collect == false(), Bangumi.deleted == false())
|
||||
)
|
||||
result = await self.session.execute(condition)
|
||||
result = self.session.execute(condition)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def not_added(self) -> list[Bangumi]:
|
||||
def not_added(self) -> list[Bangumi]:
|
||||
conditions = select(Bangumi).where(
|
||||
or_(
|
||||
Bangumi.added == 0,
|
||||
@@ -192,20 +191,20 @@ class BangumiDatabase:
|
||||
Bangumi.save_path is None,
|
||||
)
|
||||
)
|
||||
result = await self.session.execute(conditions)
|
||||
result = self.session.execute(conditions)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def disable_rule(self, _id: int):
|
||||
def disable_rule(self, _id: int):
|
||||
statement = select(Bangumi).where(Bangumi.id == _id)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
bangumi = result.scalar_one_or_none()
|
||||
if bangumi:
|
||||
bangumi.deleted = True
|
||||
self.session.add(bangumi)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
logger.debug(f"[Database] Disable rule {bangumi.title_raw}.")
|
||||
|
||||
async def search_rss(self, rss_link: str) -> list[Bangumi]:
|
||||
def search_rss(self, rss_link: str) -> list[Bangumi]:
|
||||
statement = select(Bangumi).where(func.instr(rss_link, Bangumi.rss_link) > 0)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import and_, delete, select
|
||||
from sqlmodel import Session, and_, delete, select
|
||||
|
||||
from module.models import RSSItem, RSSUpdate
|
||||
|
||||
@@ -9,12 +8,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RSSDatabase:
|
||||
def __init__(self, session: AsyncSession):
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
async def add(self, data: RSSItem) -> bool:
|
||||
def add(self, data: RSSItem) -> bool:
|
||||
statement = select(RSSItem).where(RSSItem.url == data.url)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
db_data = result.scalar_one_or_none()
|
||||
if db_data:
|
||||
logger.debug(f"RSS Item {data.url} already exists.")
|
||||
@@ -22,26 +21,26 @@ class RSSDatabase:
|
||||
else:
|
||||
logger.debug(f"RSS Item {data.url} not exists, adding...")
|
||||
self.session.add(data)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(data)
|
||||
self.session.commit()
|
||||
self.session.refresh(data)
|
||||
return True
|
||||
|
||||
async def add_all(self, data: list[RSSItem]):
|
||||
def add_all(self, data: list[RSSItem]):
|
||||
if not data:
|
||||
return
|
||||
urls = [item.url for item in data]
|
||||
statement = select(RSSItem.url).where(RSSItem.url.in_(urls))
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
existing_urls = set(result.scalars().all())
|
||||
new_items = [item for item in data if item.url not in existing_urls]
|
||||
if new_items:
|
||||
self.session.add_all(new_items)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
logger.debug(f"Batch inserted {len(new_items)} RSS items.")
|
||||
|
||||
async def update(self, _id: int, data: RSSUpdate) -> bool:
|
||||
def update(self, _id: int, data: RSSUpdate) -> bool:
|
||||
statement = select(RSSItem).where(RSSItem.id == _id)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
db_data = result.scalar_one_or_none()
|
||||
if not db_data:
|
||||
return False
|
||||
@@ -49,61 +48,61 @@ class RSSDatabase:
|
||||
for key, value in dict_data.items():
|
||||
setattr(db_data, key, value)
|
||||
self.session.add(db_data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
async def enable(self, _id: int) -> bool:
|
||||
def enable(self, _id: int) -> bool:
|
||||
statement = select(RSSItem).where(RSSItem.id == _id)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
db_data = result.scalar_one_or_none()
|
||||
if not db_data:
|
||||
return False
|
||||
db_data.enabled = True
|
||||
self.session.add(db_data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
async def disable(self, _id: int) -> bool:
|
||||
def disable(self, _id: int) -> bool:
|
||||
statement = select(RSSItem).where(RSSItem.id == _id)
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
db_data = result.scalar_one_or_none()
|
||||
if not db_data:
|
||||
return False
|
||||
db_data.enabled = False
|
||||
self.session.add(db_data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
async def search_id(self, _id: int) -> RSSItem | None:
|
||||
return await self.session.get(RSSItem, _id)
|
||||
def search_id(self, _id: int) -> RSSItem | None:
|
||||
return self.session.get(RSSItem, _id)
|
||||
|
||||
async def search_all(self) -> list[RSSItem]:
|
||||
result = await self.session.execute(select(RSSItem))
|
||||
def search_all(self) -> list[RSSItem]:
|
||||
result = self.session.execute(select(RSSItem))
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def search_active(self) -> list[RSSItem]:
|
||||
result = await self.session.execute(
|
||||
def search_active(self) -> list[RSSItem]:
|
||||
result = self.session.execute(
|
||||
select(RSSItem).where(RSSItem.enabled)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def search_aggregate(self) -> list[RSSItem]:
|
||||
result = await self.session.execute(
|
||||
def search_aggregate(self) -> list[RSSItem]:
|
||||
result = self.session.execute(
|
||||
select(RSSItem).where(and_(RSSItem.aggregate, RSSItem.enabled))
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def delete(self, _id: int) -> bool:
|
||||
def delete(self, _id: int) -> bool:
|
||||
condition = delete(RSSItem).where(RSSItem.id == _id)
|
||||
try:
|
||||
await self.session.execute(condition)
|
||||
await self.session.commit()
|
||||
self.session.execute(condition)
|
||||
self.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Delete RSS Item failed. Because: {e}")
|
||||
return False
|
||||
|
||||
async def delete_all(self):
|
||||
def delete_all(self):
|
||||
condition = delete(RSSItem)
|
||||
await self.session.execute(condition)
|
||||
await self.session.commit()
|
||||
self.session.execute(condition)
|
||||
self.session.commit()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from module.models import Torrent
|
||||
|
||||
@@ -9,54 +8,54 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TorrentDatabase:
|
||||
def __init__(self, session: AsyncSession):
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
async def add(self, data: Torrent):
|
||||
def add(self, data: Torrent):
|
||||
self.session.add(data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
logger.debug(f"Insert {data.name} in database.")
|
||||
|
||||
async def add_all(self, datas: list[Torrent]):
|
||||
def add_all(self, datas: list[Torrent]):
|
||||
self.session.add_all(datas)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
logger.debug(f"Insert {len(datas)} torrents in database.")
|
||||
|
||||
async def update(self, data: Torrent):
|
||||
def update(self, data: Torrent):
|
||||
self.session.add(data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
logger.debug(f"Update {data.name} in database.")
|
||||
|
||||
async def update_all(self, datas: list[Torrent]):
|
||||
def update_all(self, datas: list[Torrent]):
|
||||
self.session.add_all(datas)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
|
||||
async def update_one_user(self, data: Torrent):
|
||||
def update_one_user(self, data: Torrent):
|
||||
self.session.add(data)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
logger.debug(f"Update {data.name} in database.")
|
||||
|
||||
async def search(self, _id: int) -> Torrent | None:
|
||||
result = await self.session.execute(
|
||||
def search(self, _id: int) -> Torrent | None:
|
||||
result = self.session.execute(
|
||||
select(Torrent).where(Torrent.id == _id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def search_all(self) -> list[Torrent]:
|
||||
result = await self.session.execute(select(Torrent))
|
||||
def search_all(self) -> list[Torrent]:
|
||||
result = self.session.execute(select(Torrent))
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def search_rss(self, rss_id: int) -> list[Torrent]:
|
||||
result = await self.session.execute(
|
||||
def search_rss(self, rss_id: int) -> list[Torrent]:
|
||||
result = self.session.execute(
|
||||
select(Torrent).where(Torrent.rss_id == rss_id)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def check_new(self, torrents_list: list[Torrent]) -> list[Torrent]:
|
||||
def check_new(self, torrents_list: list[Torrent]) -> list[Torrent]:
|
||||
if not torrents_list:
|
||||
return []
|
||||
urls = [t.url for t in torrents_list]
|
||||
statement = select(Torrent.url).where(Torrent.url.in_(urls))
|
||||
result = await self.session.execute(statement)
|
||||
result = self.session.execute(statement)
|
||||
existing_urls = set(result.scalars().all())
|
||||
return [t for t in torrents_list if t.url not in existing_urls]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from module.models import ResponseModel
|
||||
from module.models.user import User, UserUpdate
|
||||
@@ -12,21 +11,21 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserDatabase:
|
||||
def __init__(self, session: AsyncSession):
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
async def get_user(self, username: str) -> User:
|
||||
def get_user(self, username: str) -> User:
|
||||
statement = select(User).where(User.username == username)
|
||||
result = await self.session.execute(statement)
|
||||
user = result.scalar_one_or_none()
|
||||
result = self.session.exec(statement)
|
||||
user = result.first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
async def auth_user(self, user: User) -> ResponseModel:
|
||||
def auth_user(self, user: User) -> ResponseModel:
|
||||
statement = select(User).where(User.username == user.username)
|
||||
result = await self.session.execute(statement)
|
||||
db_user = result.scalar_one_or_none()
|
||||
result = self.session.exec(statement)
|
||||
db_user = result.first()
|
||||
if not user.password:
|
||||
return ResponseModel(
|
||||
status_code=401,
|
||||
@@ -55,10 +54,10 @@ class UserDatabase:
|
||||
msg_zh="登录成功",
|
||||
)
|
||||
|
||||
async def update_user(self, username: str, update_user: UserUpdate) -> User:
|
||||
def update_user(self, username: str, update_user: UserUpdate) -> User:
|
||||
statement = select(User).where(User.username == username)
|
||||
result = await self.session.execute(statement)
|
||||
db_user = result.scalar_one_or_none()
|
||||
result = self.session.exec(statement)
|
||||
db_user = result.first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if update_user.username:
|
||||
@@ -66,18 +65,18 @@ class UserDatabase:
|
||||
if update_user.password:
|
||||
db_user.password = get_password_hash(update_user.password)
|
||||
self.session.add(db_user)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
return db_user
|
||||
|
||||
async def add_default_user(self):
|
||||
def add_default_user(self):
|
||||
statement = select(User)
|
||||
try:
|
||||
result = await self.session.execute(statement)
|
||||
users = list(result.scalars().all())
|
||||
result = self.session.exec(statement)
|
||||
users = list(result.all())
|
||||
except Exception:
|
||||
users = []
|
||||
if len(users) != 0:
|
||||
return
|
||||
user = User(username="admin", password=get_password_hash("adminadmin"))
|
||||
self.session.add(user)
|
||||
await self.session.commit()
|
||||
self.session.commit()
|
||||
|
||||
Reference in New Issue
Block a user