inspirations
This commit is contained in:
@@ -42,8 +42,9 @@ async def get_asset(
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Asset not found")
|
||||
|
||||
headers = {
|
||||
"Cache-Control": "public, max-age=31536000, immutable"
|
||||
base_headers = {
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"Accept-Ranges": "bytes"
|
||||
}
|
||||
|
||||
# Thumbnail: маленький, можно грузить в RAM
|
||||
@@ -51,17 +52,70 @@ async def get_asset(
|
||||
if asset.minio_thumbnail_object_name and s3_adapter:
|
||||
thumb_bytes = await s3_adapter.get_file(asset.minio_thumbnail_object_name)
|
||||
if thumb_bytes:
|
||||
return Response(content=thumb_bytes, media_type="image/jpeg", headers=headers)
|
||||
return Response(content=thumb_bytes, media_type="image/jpeg", headers=base_headers)
|
||||
# Fallback: thumbnail in DB
|
||||
if asset.thumbnail:
|
||||
return Response(content=asset.thumbnail, media_type="image/jpeg", headers=headers)
|
||||
return Response(content=asset.thumbnail, media_type="image/jpeg", headers=base_headers)
|
||||
# No thumbnail available — fall through to main content
|
||||
|
||||
# Main content: стримим из S3 без загрузки в RAM
|
||||
if asset.minio_object_name and s3_adapter:
|
||||
content_type = "image/png"
|
||||
# if asset.content_type == AssetContentType.VIDEO:
|
||||
# content_type = "video/mp4"
|
||||
if asset.content_type == AssetContentType.VIDEO:
|
||||
content_type = "video/mp4" # Or detect from extension if stored
|
||||
elif asset.content_type == AssetContentType.IMAGE:
|
||||
content_type = "image/png" # Default for images
|
||||
|
||||
# Better content type detection based on extension if possible, but for now this is okay
|
||||
if asset.minio_object_name.endswith(".mp4"):
|
||||
content_type = "video/mp4"
|
||||
elif asset.minio_object_name.endswith(".mov"):
|
||||
content_type = "video/quicktime"
|
||||
elif asset.minio_object_name.endswith(".jpg") or asset.minio_object_name.endswith(".jpeg"):
|
||||
content_type = "image/jpeg"
|
||||
|
||||
# Handle Range requests for video streaming
|
||||
range_header = request.headers.get("range")
|
||||
file_size = await s3_adapter.get_file_size(asset.minio_object_name)
|
||||
|
||||
if range_header and file_size:
|
||||
try:
|
||||
# Parse Range header: bytes=start-end
|
||||
byte_range = range_header.replace("bytes=", "")
|
||||
start_str, end_str = byte_range.split("-")
|
||||
start = int(start_str)
|
||||
end = int(end_str) if end_str else file_size - 1
|
||||
|
||||
# Validate range
|
||||
if start >= file_size:
|
||||
# 416 Range Not Satisfiable
|
||||
return Response(status_code=416, headers={"Content-Range": f"bytes */{file_size}"})
|
||||
|
||||
chunk_size = end - start + 1
|
||||
|
||||
headers = base_headers.copy()
|
||||
headers.update({
|
||||
"Content-Range": f"bytes {start}-{end}/{file_size}",
|
||||
"Content-Length": str(chunk_size),
|
||||
})
|
||||
|
||||
# Pass the exact range string to S3
|
||||
s3_range = f"bytes={start}-{end}"
|
||||
|
||||
return StreamingResponse(
|
||||
s3_adapter.stream_file(asset.minio_object_name, range_header=s3_range),
|
||||
status_code=206,
|
||||
headers=headers,
|
||||
media_type=content_type
|
||||
)
|
||||
except ValueError:
|
||||
pass # Fallback to full content if range parsing fails
|
||||
|
||||
# Full content response
|
||||
headers = base_headers.copy()
|
||||
if file_size:
|
||||
headers["Content-Length"] = str(file_size)
|
||||
|
||||
return StreamingResponse(
|
||||
s3_adapter.stream_file(asset.minio_object_name),
|
||||
media_type=content_type,
|
||||
@@ -70,7 +124,7 @@ async def get_asset(
|
||||
|
||||
# Fallback: data stored in DB (legacy)
|
||||
if asset.data:
|
||||
return Response(content=asset.data, media_type="image/png", headers=headers)
|
||||
return Response(content=asset.data, media_type="image/png", headers=base_headers)
|
||||
|
||||
raise HTTPException(status_code=404, detail="Asset data not found")
|
||||
|
||||
|
||||
@@ -20,7 +20,13 @@ async def create_idea(
|
||||
):
|
||||
pid = project_id or request.project_id
|
||||
|
||||
return await idea_service.create_idea(request.name, request.description, pid, str(current_user["_id"]))
|
||||
return await idea_service.create_idea(
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
project_id=pid,
|
||||
user_id=str(current_user["_id"]),
|
||||
inspiration_id=request.inspiration_id
|
||||
)
|
||||
|
||||
@router.get("", response_model=List[IdeaResponse])
|
||||
async def get_ideas(
|
||||
@@ -48,7 +54,12 @@ async def update_idea(
|
||||
request: IdeaUpdateRequest,
|
||||
idea_service: IdeaService = Depends(get_idea_service)
|
||||
):
|
||||
idea = await idea_service.update_idea(idea_id, request.name, request.description)
|
||||
idea = await idea_service.update_idea(
|
||||
idea_id=idea_id,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
inspiration_id=request.inspiration_id
|
||||
)
|
||||
if not idea:
|
||||
raise HTTPException(status_code=404, detail="Idea not found")
|
||||
return idea
|
||||
|
||||
95
api/endpoints/inspiration_router.py
Normal file
95
api/endpoints/inspiration_router.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from api.dependency import get_inspiration_service, get_project_id
|
||||
from api.endpoints.auth import get_current_user
|
||||
from api.models.InspirationRequest import InspirationCreateRequest, InspirationResponse, InspirationListResponse
|
||||
from api.service.inspiration_service import InspirationService
|
||||
from models.Inspiration import Inspiration
|
||||
|
||||
router = APIRouter(prefix="/api/inspirations", tags=["Inspirations"])
|
||||
|
||||
|
||||
@router.post("", response_model=InspirationResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_inspiration(
|
||||
request: InspirationCreateRequest,
|
||||
project_id: Optional[str] = Depends(get_project_id),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
service: InspirationService = Depends(get_inspiration_service)
|
||||
):
|
||||
pid = project_id or request.project_id
|
||||
|
||||
inspiration = await service.create_inspiration(
|
||||
source_url=request.source_url,
|
||||
created_by=str(current_user["_id"]),
|
||||
project_id=pid,
|
||||
caption=request.caption
|
||||
)
|
||||
return inspiration
|
||||
|
||||
|
||||
@router.get("", response_model=InspirationListResponse)
|
||||
async def get_inspirations(
|
||||
project_id: Optional[str] = Depends(get_project_id),
|
||||
limit: int = 20,
|
||||
offset: int = 0,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
service: InspirationService = Depends(get_inspiration_service)
|
||||
):
|
||||
# If project_id is provided, filter by it. Otherwise, filter by user.
|
||||
# Or maybe we want to see all user's inspirations if no project is selected?
|
||||
# Let's follow the pattern: if project_id is present, show project's inspirations.
|
||||
# If not, show user's personal inspirations (where project_id is None) OR all user's inspirations?
|
||||
# Usually "My Inspirations" means created by me.
|
||||
|
||||
# Let's assume:
|
||||
# If project_id -> filter by project_id (and maybe created_by if we want strict ownership, but usually project members share)
|
||||
# If no project_id -> filter by created_by (personal)
|
||||
|
||||
pid = project_id
|
||||
uid = str(current_user["_id"])
|
||||
|
||||
inspirations = await service.get_inspirations(project_id=pid, created_by=uid if not pid else None, limit=limit, offset=offset)
|
||||
total_count = await service.dao.inspirations.count_inspirations(project_id=pid, created_by=uid if not pid else None)
|
||||
|
||||
return InspirationListResponse(
|
||||
inspirations=[InspirationResponse(**inspiration.model_dump()) for inspiration in inspirations],
|
||||
total_count=total_count
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{inspiration_id}", response_model=InspirationResponse)
|
||||
async def get_inspiration(
|
||||
inspiration_id: str,
|
||||
service: InspirationService = Depends(get_inspiration_service),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
inspiration = await service.get_inspiration(inspiration_id)
|
||||
if not inspiration:
|
||||
raise HTTPException(status_code=404, detail="Inspiration not found")
|
||||
return inspiration
|
||||
|
||||
|
||||
@router.patch("/{inspiration_id}/complete", response_model=InspirationResponse)
|
||||
async def mark_inspiration_complete(
|
||||
inspiration_id: str,
|
||||
is_completed: bool = True,
|
||||
service: InspirationService = Depends(get_inspiration_service),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
inspiration = await service.mark_as_completed(inspiration_id, is_completed)
|
||||
if not inspiration:
|
||||
raise HTTPException(status_code=404, detail="Inspiration not found")
|
||||
return inspiration
|
||||
|
||||
|
||||
@router.delete("/{inspiration_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_inspiration(
|
||||
inspiration_id: str,
|
||||
service: InspirationService = Depends(get_inspiration_service),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
success = await service.delete_inspiration(inspiration_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Inspiration not found")
|
||||
return None
|
||||
Reference in New Issue
Block a user