style: apply black formatting to pass CI validation (#126)
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled

This commit was merged in pull request #126.
This commit is contained in:
2026-06-30 17:23:08 +08:00
parent 1e6f78a346
commit 52ff2f80ad
107 changed files with 1517 additions and 1010 deletions
+2 -2
View File
@@ -49,7 +49,7 @@ def list_asset_libraries(
) -> ListAssetLibrariesResponse:
user_id = authenticated_user.user.id
use_case = ListAssetLibrariesUseCase(asset_library_repository)
if project_id:
# If project_id provided, check access and filter by project
project = GetProjectUseCase(project_repository).execute(project_id)
@@ -65,7 +65,7 @@ def list_asset_libraries(
for proj in accessible_projects:
all_items.extend(use_case.execute(proj.id))
items = all_items
return ListAssetLibrariesResponse(items=[_to_asset_library_response(item) for item in items])
+1 -1
View File
@@ -106,7 +106,7 @@ def create_asset(
raise HTTPException(status_code=404, detail=f"Project {request.project_id} not found")
if not project.can_access(authenticated_user.user.id):
raise HTTPException(status_code=403, detail="Access denied to project")
library = asset_library_repository.get(request.library_id)
if library is None or library.project_id != request.project_id:
raise HTTPException(status_code=404, detail=f"AssetLibrary {request.library_id} not found")
+1 -1
View File
@@ -55,6 +55,7 @@ class LoginRequest(BaseModel):
class RefreshRequest(BaseModel):
refresh_token: str
class LoginResponse(BaseModel):
access_token: str
refresh_token: str
@@ -173,7 +174,6 @@ async def refresh(
)
def _verify_email_token(token: str, user_repository: UserRepository) -> MessageResponse:
success, error = VerifyEmailUseCase(user_repository=user_repository).execute(VerifyEmailRequest(token=token))
if not success:
+17 -6
View File
@@ -44,9 +44,18 @@ CHUNK_EXPIRY_HOURS = 24
# Allowed file types (consistent with existing upload.py)
ALLOWED_MIME_TYPES = {
"image/jpeg", "image/png", "image/gif", "image/webp",
"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm",
"audio/mpeg", "audio/wav", "audio/ogg", "audio/mp3",
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"video/mp4",
"video/quicktime",
"video/x-msvideo",
"video/webm",
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/mp3",
}
# Chunk storage root directory
@@ -67,13 +76,13 @@ def _atomic_check_and_record(upload_id: str, chunk_index: int) -> bool:
"""
Atomically check if chunk is uploaded and record if not.
Uses file locking to prevent race conditions.
Returns:
True if chunk was newly recorded, False if already exists
"""
meta_path = _get_upload_meta_path(upload_id)
CHUNK_STORAGE_ROOT.mkdir(parents=True, exist_ok=True)
with open(meta_path, "r+", encoding="utf-8") as f:
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
try:
@@ -128,15 +137,17 @@ def _validate_file_type(content: bytes, filename: str) -> str:
"""Validate file type"""
try:
import magic
detected_mime = magic.from_buffer(content, mime=True)
except ImportError:
import mimetypes
detected_mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
if detected_mime not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported file type: {detected_mime}. Allowed types: {', '.join(sorted(ALLOWED_MIME_TYPES))}"
detail=f"Unsupported file type: {detected_mime}. Allowed types: {', '.join(sorted(ALLOWED_MIME_TYPES))}",
)
return detected_mime
+18 -9
View File
@@ -1,4 +1,5 @@
"""查重 API 路由。"""
from __future__ import annotations
import logging
@@ -31,10 +32,17 @@ logger = logging.getLogger(__name__)
router = APIRouter()
# 查重功能只接受视频文件
ALLOWED_VIDEO_MIME_TYPES = frozenset({
"video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo",
"video/webm", "video/x-matroska", "video/3gpp",
})
ALLOWED_VIDEO_MIME_TYPES = frozenset(
{
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/x-msvideo",
"video/webm",
"video/x-matroska",
"video/3gpp",
}
)
def _validate_video_mime_type(content_type: str | None) -> str:
@@ -44,16 +52,16 @@ def _validate_video_mime_type(content_type: str | None) -> str:
status_code=status.HTTP_400_BAD_REQUEST,
detail="Content-Type header is required",
)
# 处理带参数的类型,如 "video/mp4; charset=utf-8"
base_type = content_type.split(";")[0].strip().lower()
if base_type not in ALLOWED_VIDEO_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail=f"只支持视频文件。支持的类型: mp4, mpeg, mov, avi, webm, mkv, 3gp",
)
return base_type
@@ -117,9 +125,10 @@ async def upload_for_duplication(
# P0-2: 验证文件大小(参考 OSS_DIRECT_UPLOAD_MAX_MB)
from app.config import get_settings
settings = get_settings()
max_size_bytes = settings.OSS_DIRECT_UPLOAD_MAX_MB * 1024 * 1024
# 先检查 Content-Length header(如果可用)
if file.size is not None and file.size > max_size_bytes:
raise HTTPException(
@@ -135,7 +144,7 @@ async def upload_for_duplication(
try:
content = await file.read()
file_size = len(content)
# 再次检查实际文件大小
if file_size > max_size_bytes:
raise HTTPException(
+2 -2
View File
@@ -50,7 +50,7 @@ def list_generated_videos(
) -> ListGeneratedVideosResponse:
user_id = authenticated_user.user.id
use_case = ListGeneratedVideosUseCase(generated_video_repository)
if project_id:
# If project_id provided, check access and filter by project
project = project_repository.find_by_id(project_id)
@@ -64,7 +64,7 @@ def list_generated_videos(
for proj in accessible_projects:
all_items.extend(use_case.execute(proj.id))
items = all_items
# Generate download URLs for each video
responses = []
for item in items:
+2 -4
View File
@@ -1,4 +1,5 @@
"""Recipe CRUD + use routes."""
from __future__ import annotations
from typing import List
@@ -209,8 +210,5 @@ def use_recipe(
return UseRecipeResponse(
recipe=_to_response(result.recipe),
warnings=[
{"item_type": w.item_type, "item_id": w.item_id, "position": w.position}
for w in result.warnings
],
warnings=[{"item_type": w.item_type, "item_id": w.item_id, "position": w.position} for w in result.warnings],
)
+3
View File
@@ -1,4 +1,5 @@
"""Subscription management API routes."""
from __future__ import annotations
from dataclasses import replace
@@ -34,6 +35,7 @@ PLAN_QUOTAS = {
# ============ Helper Functions ============
def _get_plan_name(plan_id: str) -> str:
"""获取套餐显示名称"""
plan_names = {
@@ -86,6 +88,7 @@ def _build_subscription_info(user: AuthenticatedUser) -> SubscriptionInfo:
# ============ API Endpoints ============
@router.get("/current", response_model=SubscriptionInfo)
async def get_current_subscription(
current_user: AuthenticatedUser = Depends(get_current_user),
+7 -9
View File
@@ -1,4 +1,5 @@
"""Template CRUD + generate + category routes."""
from __future__ import annotations
from typing import List
@@ -234,10 +235,7 @@ def validate_template(
return ValidateTemplateResponse(
template=_to_response(result.template),
warnings=[
GenerateWarningResponse(code=w.code, message=w.message, details=w.details)
for w in result.warnings
],
warnings=[GenerateWarningResponse(code=w.code, message=w.message, details=w.details) for w in result.warnings],
)
@@ -253,10 +251,7 @@ def list_categories(
use_case = ListCategoriesUseCase(template_repository)
categories = use_case.execute(user_id)
return ListCategoriesResponse(
items=[
CategoryResponse(id=c.id, user_id=c.user_id, name=c.name, created_at=c.created_at)
for c in categories
],
items=[CategoryResponse(id=c.id, user_id=c.user_id, name=c.name, created_at=c.created_at) for c in categories],
)
@@ -271,7 +266,10 @@ def create_category(
use_case = CreateCategoryUseCase(template_repository)
category = use_case.execute(command)
return CategoryResponse(
id=category.id, user_id=category.user_id, name=category.name, created_at=category.created_at,
id=category.id,
user_id=category.user_id,
name=category.name,
created_at=category.created_at,
)
+1
View File
@@ -1,4 +1,5 @@
"""Title library CRUD routes."""
from __future__ import annotations
from typing import Optional
+32 -14
View File
@@ -30,17 +30,35 @@ logger = logging.getLogger(__name__)
router = APIRouter()
# 允许上传的文件 MIME 类型
ALLOWED_MIME_TYPES = frozenset({
# 视频
"video/mp4", "video/mpeg", "video/quicktime", "video/x-msvideo",
"video/webm", "video/x-matroska", "video/3gpp",
# 音频
"audio/mpeg", "audio/wav", "audio/ogg", "audio/flac", "audio/aac",
"audio/mp3", "audio/x-m4a", "audio/webm",
# 图片
"image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp",
"image/svg+xml", "image/tiff",
})
ALLOWED_MIME_TYPES = frozenset(
{
# 视频
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/x-msvideo",
"video/webm",
"video/x-matroska",
"video/3gpp",
# 音频
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/flac",
"audio/aac",
"audio/mp3",
"audio/x-m4a",
"audio/webm",
# 图片
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/bmp",
"image/svg+xml",
"image/tiff",
}
)
def _validate_mime_type(content_type: str | None) -> str:
@@ -50,16 +68,16 @@ def _validate_mime_type(content_type: str | None) -> str:
status_code=status.HTTP_400_BAD_REQUEST,
detail="Content-Type header is required",
)
# 处理带参数的类型,如 "video/mp4; charset=utf-8"
base_type = content_type.split(";")[0].strip().lower()
if base_type not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail=f"File type '{base_type}' is not supported. Allowed types: video, audio, and image files.",
)
return base_type
+1
View File
@@ -1,4 +1,5 @@
"""Voice library CRUD routes."""
from __future__ import annotations
from typing import Optional