Files
hot_comment_radar/app/main.py
T

162 lines
6.8 KiB
Python

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from urllib.parse import quote
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.responses import HTMLResponse
from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.db import SessionLocal, get_db_session, init_db
from app.models import Comment, ContentItem, Hotspot, Report
from app.schemas import CreateTaskRequest, CreateTaskResponse, TaskResponse
from app.services.export_service import export_hotspot_comments_csv, export_item_comments_csv, export_report_markdown
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
@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.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},
)
@app.get("/api/export/items/{item_id}/comments.csv")
def export_item_comments(item_id: str, session: Session = Depends(get_db_session)) -> Response:
try:
content, filename = export_item_comments_csv(session, item_id)
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="导出数据不存在")
return Response(
content=content,
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"},
)
@app.get("/api/export/hotspots/{hotspot_id}/comments.csv")
def export_hotspot_comments(hotspot_id: str, session: Session = Depends(get_db_session)) -> Response:
try:
content, filename = export_hotspot_comments_csv(session, hotspot_id)
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="导出数据不存在")
return Response(
content=content,
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"},
)
@app.get("/api/export/hotspots/{hotspot_id}.md")
def export_hotspot_markdown(hotspot_id: str, session: Session = Depends(get_db_session)) -> Response:
try:
content, filename = export_report_markdown(session, report_type="hotspot", hotspot_id=hotspot_id)
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报告不存在")
return Response(content=content, media_type="text/markdown; charset=utf-8", headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"})
@app.get("/api/export/items/{item_id}.md")
def export_item_markdown(item_id: str, session: Session = Depends(get_db_session)) -> Response:
try:
content, filename = export_report_markdown(session, report_type="item", item_id=item_id)
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报告不存在")
return Response(content=content, media_type="text/markdown; charset=utf-8", headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"})