mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-06 20:42:52 +08:00
- 实现本地视频上传和处理功能 - 新增 LocalDownloader 类处理本地视频 - 更新前端界面支持本地视频选择 - 添加视频封面提取和保存功能 - 优化后端路由支持本地视频上传
231 lines
7.6 KiB
Python
231 lines
7.6 KiB
Python
# app/routers/note.py
|
||
import json
|
||
import os
|
||
import uuid
|
||
from typing import Optional
|
||
from urllib.parse import urlparse
|
||
|
||
from fastapi import APIRouter, HTTPException, BackgroundTasks, UploadFile, File
|
||
from pydantic import BaseModel, validator
|
||
from dataclasses import asdict
|
||
|
||
from app.db.video_task_dao import get_task_by_video
|
||
from app.enmus.note_enums import DownloadQuality
|
||
from app.services.note import NoteGenerator, logger
|
||
from app.utils.response import ResponseWrapper as R
|
||
from app.utils.url_parser import extract_video_id
|
||
from app.validators.video_url_validator import is_supported_video_url
|
||
from fastapi import APIRouter, Request, HTTPException
|
||
from fastapi.responses import StreamingResponse
|
||
import httpx
|
||
from app.enmus.task_status_enums import TaskStatus
|
||
|
||
# from app.services.downloader import download_raw_audio
|
||
# from app.services.whisperer import transcribe_audio
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
class RecordRequest(BaseModel):
|
||
video_id: str
|
||
platform: str
|
||
|
||
|
||
class VideoRequest(BaseModel):
|
||
video_url: str
|
||
platform: str
|
||
quality: DownloadQuality
|
||
screenshot: Optional[bool] = False
|
||
link: Optional[bool] = False
|
||
model_name:str
|
||
provider_id:str
|
||
task_id: Optional[str] = None
|
||
format:Optional[list]=[]
|
||
style:str=None
|
||
extras:Optional[str]
|
||
|
||
@validator("video_url")
|
||
def validate_supported_url(cls, v):
|
||
url = str(v)
|
||
parsed = urlparse(url)
|
||
if parsed.scheme in ("http", "https"):
|
||
# 是网络链接,继续用原有平台校验
|
||
if not is_supported_video_url(url):
|
||
raise ValueError("暂不支持该视频平台或链接格式无效")
|
||
else:
|
||
# 是本地路径,检测一下文件是否存在
|
||
|
||
if not url.startswith('/uploads') and not os.path.exists(url):
|
||
raise ValueError("本地文件路径不存在")
|
||
return v
|
||
|
||
|
||
NOTE_OUTPUT_DIR = "note_results"
|
||
UPLOAD_DIR = "uploads"
|
||
|
||
def save_note_to_file(task_id: str, note):
|
||
os.makedirs(NOTE_OUTPUT_DIR, exist_ok=True)
|
||
with open(os.path.join(NOTE_OUTPUT_DIR, f"{task_id}.json"), "w", encoding="utf-8") as f:
|
||
json.dump(asdict(note), f, ensure_ascii=False, indent=2)
|
||
|
||
|
||
def run_note_task(task_id: str, video_url: str, platform: str, quality: DownloadQuality,
|
||
link: bool = False,screenshot: bool = False,model_name:str=None,provider_id:str=None,
|
||
_format:list=None,style:str=None,extras:str=None):
|
||
try:
|
||
if not model_name or not provider_id:
|
||
raise HTTPException(status_code=400, detail="请选择模型和提供者")
|
||
|
||
note = NoteGenerator().generate(
|
||
video_url=video_url,
|
||
platform=platform,
|
||
quality=quality,
|
||
task_id=task_id,
|
||
model_name=model_name,
|
||
provider_id=provider_id,
|
||
link=link,
|
||
_format=_format,
|
||
style=style,
|
||
extras=extras,
|
||
screenshot=screenshot
|
||
)
|
||
print('Note 结果',note)
|
||
save_note_to_file(task_id, note)
|
||
except Exception as e:
|
||
save_note_to_file(task_id, {"error": str(e)})
|
||
|
||
|
||
@router.post('/delete_task')
|
||
def delete_task(data:RecordRequest):
|
||
try:
|
||
|
||
NoteGenerator().delete_note(video_id=data.video_id,platform=data.platform)
|
||
return R.success(msg='删除成功')
|
||
except Exception as e:
|
||
return R.error(msg=e)
|
||
|
||
|
||
|
||
|
||
@router.post("/upload")
|
||
async def upload(file: UploadFile = File(...)):
|
||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||
file_location = os.path.join(UPLOAD_DIR, file.filename)
|
||
|
||
with open(file_location, "wb+") as f:
|
||
f.write(await file.read())
|
||
|
||
# 假设你静态目录挂载了 /uploads
|
||
return R.success({"url": f"/uploads/{file.filename}"})
|
||
|
||
@router.post("/generate_note")
|
||
def generate_note(data: VideoRequest, background_tasks: BackgroundTasks):
|
||
try:
|
||
|
||
video_id = extract_video_id(data.video_url, data.platform)
|
||
# if not video_id:
|
||
# raise HTTPException(status_code=400, detail="无法提取视频 ID")
|
||
# existing = get_task_by_video(video_id, data.platform)
|
||
# if existing:
|
||
# return R.error(
|
||
# msg='笔记已生成,请勿重复发起',
|
||
#
|
||
# )
|
||
|
||
|
||
if data.task_id:
|
||
# 如果传了task_id,说明是重试!
|
||
task_id = data.task_id
|
||
# 更新之前的状态
|
||
NoteGenerator.update_task_status(task_id, TaskStatus.PENDING)
|
||
logger.info(f"重试模式,复用已有 task_id={task_id}")
|
||
else:
|
||
# 正常新建任务
|
||
task_id = str(uuid.uuid4())
|
||
|
||
background_tasks.add_task(run_note_task, task_id, data.video_url, data.platform, data.quality,data.link ,data.screenshot,data.model_name,data.provider_id,data.format,data.style,data.extras)
|
||
return R.success({"task_id": task_id})
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
|
||
|
||
@router.get("/task_status/{task_id}")
|
||
def get_task_status(task_id: str):
|
||
status_path = os.path.join(NOTE_OUTPUT_DIR, f"{task_id}.status.json")
|
||
result_path = os.path.join(NOTE_OUTPUT_DIR, f"{task_id}.json")
|
||
|
||
# 优先读状态文件
|
||
if os.path.exists(status_path):
|
||
with open(status_path, "r", encoding="utf-8") as f:
|
||
status_content = json.load(f)
|
||
|
||
status = status_content.get("status")
|
||
message = status_content.get("message", "")
|
||
|
||
if status == TaskStatus.SUCCESS.value:
|
||
# 成功状态的话,继续读取最终笔记内容
|
||
if os.path.exists(result_path):
|
||
with open(result_path, "r", encoding="utf-8") as rf:
|
||
result_content = json.load(rf)
|
||
return R.success({
|
||
"status": status,
|
||
"result": result_content,
|
||
"message": message,
|
||
"task_id": task_id
|
||
})
|
||
else:
|
||
# 理论上不会出现,保险处理
|
||
return R.success({
|
||
"status": TaskStatus.PENDING.value,
|
||
"message": "任务完成,但结果文件未找到",
|
||
"task_id": task_id
|
||
})
|
||
|
||
if status == TaskStatus.FAILED.value:
|
||
return R.error(message or "任务失败", code=500)
|
||
|
||
# 处理中状态
|
||
return R.success({
|
||
"status": status,
|
||
"message": message,
|
||
"task_id": task_id
|
||
})
|
||
|
||
# 没有状态文件,但有结果
|
||
if os.path.exists(result_path):
|
||
with open(result_path, "r", encoding="utf-8") as f:
|
||
result_content = json.load(f)
|
||
return R.success({
|
||
"status": TaskStatus.SUCCESS.value,
|
||
"result": result_content,
|
||
"task_id": task_id
|
||
})
|
||
|
||
# 什么都没有,默认PENDING
|
||
return R.success({
|
||
"status": TaskStatus.PENDING.value,
|
||
"message": "任务排队中",
|
||
"task_id": task_id
|
||
})
|
||
|
||
|
||
@router.get("/image_proxy")
|
||
async def image_proxy(request: Request, url: str):
|
||
headers = {
|
||
"Referer": "https://www.bilibili.com/", # 模拟B站来源
|
||
"User-Agent": request.headers.get("User-Agent", ""),
|
||
}
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code != 200:
|
||
raise HTTPException(status_code=resp.status_code, detail="图片获取失败")
|
||
|
||
content_type = resp.headers.get("Content-Type", "image/jpeg")
|
||
return StreamingResponse(resp.aiter_bytes(), media_type=content_type)
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|