Files
hot_comment_radar/app/main.py
T

146 lines
5.6 KiB
Python

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import logging
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import OperationalError, SQLAlchemyError
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.db import SessionLocal, backup_sqlite_files, get_db_session, init_db
from app.models import Comment, ContentItem, Hotspot, Report
from app.schemas import CreateTaskRequest, CreateTaskResponse, TaskResponse
from app.services.task_service import (
RUNNING_TASK_MESSAGE,
create_task,
get_task,
has_running_task,
list_tasks,
recover_running_tasks,
)
from app.templating import templates
logger = logging.getLogger(__name__)
DATABASE_UNAVAILABLE_MESSAGE = "数据库暂时不可用,请稍后重试或联系维护者恢复数据。"
def handle_database_error(request: Request, exc: SQLAlchemyError):
try:
backup_sqlite_files()
except Exception:
logger.exception("Failed to back up SQLite files after database error")
if request.url.path.startswith("/api/"):
return JSONResponse(status_code=503, content={"detail": DATABASE_UNAVAILABLE_MESSAGE})
return templates.TemplateResponse(
request,
"errors/database_unavailable.html",
{"message": DATABASE_UNAVAILABLE_MESSAGE, "detail": str(exc)},
status_code=503,
)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
init_db()
with SessionLocal() as session:
recover_running_tasks(session)
yield
app = FastAPI(title="热榜评论分析工具", lifespan=lifespan)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
@app.exception_handler(OperationalError)
def operational_error_handler(request: Request, exc: OperationalError):
return handle_database_error(request, exc)
@app.exception_handler(SQLAlchemyError)
def sqlalchemy_error_handler(request: Request, exc: SQLAlchemyError):
return handle_database_error(request, exc)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/", response_class=HTMLResponse)
def index_page(request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
return templates.TemplateResponse(
request,
"index.html",
{"tasks": list_tasks(session)},
)
@app.post("/api/tasks", response_model=CreateTaskResponse, status_code=status.HTTP_201_CREATED)
def create_task_api(
request: CreateTaskRequest,
session: Session = Depends(get_db_session),
) -> CreateTaskResponse:
if has_running_task(session):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=RUNNING_TASK_MESSAGE)
task = create_task(session, request)
return CreateTaskResponse(task_id=task.id, status=task.status)
@app.get("/api/tasks", response_model=list[TaskResponse])
def list_tasks_api(session: Session = Depends(get_db_session)) -> list[TaskResponse]:
return [TaskResponse.model_validate(task) for task in list_tasks(session)]
@app.get("/api/tasks/{task_id}", response_model=TaskResponse)
def get_task_api(task_id: str, session: Session = Depends(get_db_session)) -> TaskResponse:
task = get_task(session, task_id)
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
return TaskResponse.model_validate(task)
@app.get("/tasks/{task_id}", response_class=HTMLResponse)
def task_detail_page(task_id: str, request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
task = get_task(session, task_id)
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="任务不存在")
hotspots = list(session.scalars(select(Hotspot).where(Hotspot.task_id == task.id).order_by(Hotspot.rank)))
return templates.TemplateResponse(request, "tasks/detail.html", {"task": task, "hotspots": hotspots})
@app.get("/hotspots/{hotspot_id}/report", response_class=HTMLResponse)
def hotspot_report_page(hotspot_id: str, request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
hotspot = session.get(Hotspot, hotspot_id)
if hotspot is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="热点不存在")
task = get_task(session, hotspot.task_id)
report = session.scalar(select(Report).where(Report.hotspot_id == hotspot.id, Report.report_type == "hotspot"))
return templates.TemplateResponse(request, "hotspots/report.html", {"task": task, "hotspot": hotspot, "report": report})
@app.get("/items/{item_id}", response_class=HTMLResponse)
def item_detail_page(item_id: str, request: Request, session: Session = Depends(get_db_session)) -> HTMLResponse:
item = session.get(ContentItem, item_id)
if item is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="内容条目不存在")
hotspot = session.get(Hotspot, item.hotspot_id)
task = get_task(session, item.task_id)
report = session.scalar(select(Report).where(Report.content_item_id == item.id, Report.report_type == "item"))
comments = list(
session.scalars(
select(Comment)
.where(Comment.content_item_id == item.id)
.order_by(Comment.like_count.desc().nullslast(), Comment.comment_time.desc().nullslast())
.limit(100)
)
)
return templates.TemplateResponse(
request,
"items/detail.html",
{"task": task, "hotspot": hotspot, "item": item, "report": report, "comments": comments},
)